summaryrefslogtreecommitdiffstats
path: root/mobile/android/thirdparty/com/adjust
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/thirdparty/com/adjust')
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java781
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java100
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityState.java151
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Adjust.java79
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java62
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java128
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java112
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java141
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java86
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java155
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Constants.java53
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java290
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java36
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ILogger.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java27
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java9
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LICENSE21
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LogLevel.java19
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Logger.java107
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java5
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java291
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java274
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Reflection.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java38
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Util.java202
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java10
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java82
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java12
32 files changed, 3801 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
new file mode 100644
index 000000000..8c7858b97
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
@@ -0,0 +1,781 @@
+//
+// ActivityHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static com.adjust.sdk.Constants.ACTIVITY_STATE_FILENAME;
+import static com.adjust.sdk.Constants.ATTRIBUTION_FILENAME;
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class ActivityHandler extends HandlerThread implements IActivityHandler {
+
+ private static long TIMER_INTERVAL;
+ private static long TIMER_START;
+ 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 static final String ACTIVITY_STATE_NAME = "Activity state";
+ private static final String ATTRIBUTION_NAME = "Attribution";
+
+ private SessionHandler sessionHandler;
+ private IPackageHandler packageHandler;
+ private ActivityState activityState;
+ private ILogger logger;
+ private static ScheduledExecutorService timer;
+ private boolean enabled;
+ private boolean offline;
+
+ private DeviceInfo deviceInfo;
+ private AdjustConfig adjustConfig; // always valid after construction
+ private AdjustAttribution attribution;
+ private IAttributionHandler attributionHandler;
+
+ private ActivityHandler(AdjustConfig adjustConfig) {
+ super(LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ logger = AdjustFactory.getLogger();
+ sessionHandler = new SessionHandler(getLooper(), this);
+ enabled = true;
+ init(adjustConfig);
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.INIT;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(AdjustConfig adjustConfig) {
+ this.adjustConfig = adjustConfig;
+ }
+
+ public static ActivityHandler getInstance(AdjustConfig adjustConfig) {
+ if (adjustConfig == null) {
+ AdjustFactory.getLogger().error("AdjustConfig missing");
+ return null;
+ }
+
+ if (!adjustConfig.isValid()) {
+ AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
+ return null;
+ }
+
+ ActivityHandler activityHandler = new ActivityHandler(adjustConfig);
+ return activityHandler;
+ }
+
+ @Override
+ public void trackSubsessionStart() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.START;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackSubsessionEnd() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.END;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackEvent(AdjustEvent event) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.EVENT;
+ message.obj = event;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.FINISH_TRACKING;
+ message.obj = jsonResponse;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == this.enabled) {
+ if (enabled) {
+ logger.debug("Adjust already enabled");
+ } else {
+ logger.debug("Adjust already disabled");
+ }
+ return;
+ }
+ this.enabled = enabled;
+ if (activityState != null) {
+ activityState.enabled = enabled;
+ }
+ if (enabled) {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused due to the SDK is offline");
+ } else {
+ logger.info("Resuming package handler and attribution handler to enabled the SDK");
+ }
+ trackSubsessionStart();
+ } else {
+ logger.info("Pausing package handler and attribution handler to disable the SDK");
+ trackSubsessionEnd();
+ }
+ }
+
+ @Override
+ public void setOfflineMode(boolean offline) {
+ if (offline == this.offline) {
+ if (offline) {
+ logger.debug("Adjust already in offline mode");
+ } else {
+ logger.debug("Adjust already in online mode");
+ }
+ return;
+ }
+ this.offline = offline;
+ if (offline) {
+ logger.info("Pausing package and attribution handler to put in offline mode");
+ } else {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused because the SDK is disabled");
+ } else {
+ logger.info("Resuming package handler and attribution handler to put in online mode");
+ }
+ }
+ updateStatus();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ if (activityState != null) {
+ return activityState.enabled;
+ } else {
+ return enabled;
+ }
+ }
+
+ @Override
+ public void readOpenUrl(Uri url, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.DEEP_LINK;
+ UrlClickTime urlClickTime = new UrlClickTime(url, clickTime);
+ message.obj = urlClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public boolean tryUpdateAttribution(AdjustAttribution attribution) {
+ if (attribution == null) return false;
+
+ if (attribution.equals(this.attribution)) {
+ return false;
+ }
+
+ saveAttribution(attribution);
+ launchAttributionListener();
+ return true;
+ }
+
+ private void saveAttribution(AdjustAttribution attribution) {
+ this.attribution = attribution;
+ writeAttribution();
+ }
+
+ private void launchAttributionListener() {
+ if (adjustConfig.onAttributionChangedListener == null) {
+ return;
+ }
+ Handler handler = new Handler(adjustConfig.context.getMainLooper());
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ adjustConfig.onAttributionChangedListener.onAttributionChanged(attribution);
+ }
+ };
+ handler.post(runnable);
+ }
+
+ @Override
+ public void setAskingAttribution(boolean askingAttribution) {
+ activityState.askingAttribution = askingAttribution;
+ writeActivityState();
+ }
+
+ @Override
+ public ActivityPackage getAttributionPackage() {
+ long now = System.currentTimeMillis();
+ PackageBuilder attributionBuilder = new PackageBuilder(adjustConfig,
+ deviceInfo,
+ activityState,
+ now);
+ return attributionBuilder.buildAttributionPackage();
+ }
+
+ @Override
+ public void sendReferrer(String referrer, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.SEND_REFERRER;
+ ReferrerClickTime referrerClickTime = new ReferrerClickTime(referrer, clickTime);
+ message.obj = referrerClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ private class UrlClickTime {
+ Uri url;
+ long clickTime;
+
+ UrlClickTime(Uri url, long clickTime) {
+ this.url = url;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private class ReferrerClickTime {
+ String referrer;
+ long clickTime;
+
+ ReferrerClickTime(String referrer, long clickTime) {
+ this.referrer = referrer;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private void updateStatus() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.UPDATE_STATUS;
+ sessionHandler.sendMessage(message);
+ }
+
+ private static final class SessionHandler extends Handler {
+ private static final int BASE_ADDRESS = 72630;
+ private static final int INIT = BASE_ADDRESS + 1;
+ private static final int START = BASE_ADDRESS + 2;
+ private static final int END = BASE_ADDRESS + 3;
+ private static final int EVENT = BASE_ADDRESS + 4;
+ private static final int FINISH_TRACKING = BASE_ADDRESS + 5;
+ private static final int DEEP_LINK = BASE_ADDRESS + 6;
+ private static final int SEND_REFERRER = BASE_ADDRESS + 7;
+ private static final int UPDATE_STATUS = BASE_ADDRESS + 8;
+
+ private final WeakReference<ActivityHandler> sessionHandlerReference;
+
+ protected SessionHandler(Looper looper, ActivityHandler sessionHandler) {
+ super(looper);
+ this.sessionHandlerReference = new WeakReference<ActivityHandler>(sessionHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ ActivityHandler sessionHandler = sessionHandlerReference.get();
+ if (sessionHandler == null) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ sessionHandler.initInternal();
+ break;
+ case START:
+ sessionHandler.startInternal();
+ break;
+ case END:
+ sessionHandler.endInternal();
+ break;
+ case EVENT:
+ AdjustEvent event = (AdjustEvent) message.obj;
+ sessionHandler.trackEventInternal(event);
+ break;
+ case FINISH_TRACKING:
+ JSONObject jsonResponse = (JSONObject) message.obj;
+ sessionHandler.finishedTrackingActivityInternal(jsonResponse);
+ break;
+ case DEEP_LINK:
+ UrlClickTime urlClickTime = (UrlClickTime) message.obj;
+ sessionHandler.readOpenUrlInternal(urlClickTime.url, urlClickTime.clickTime);
+ break;
+ case SEND_REFERRER:
+ ReferrerClickTime referrerClickTime = (ReferrerClickTime) message.obj;
+ sessionHandler.sendReferrerInternal(referrerClickTime.referrer, referrerClickTime.clickTime);
+ break;
+ case UPDATE_STATUS:
+ sessionHandler.updateStatusInternal();
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ TIMER_INTERVAL = AdjustFactory.getTimerInterval();
+ TIMER_START = AdjustFactory.getTimerStart();
+ SESSION_INTERVAL = AdjustFactory.getSessionInterval();
+ SUBSESSION_INTERVAL = AdjustFactory.getSubsessionInterval();
+
+ deviceInfo = new DeviceInfo(adjustConfig.context, adjustConfig.sdkPrefix);
+
+ if (adjustConfig.environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.setLogLevel(LogLevel.ASSERT);
+ } else {
+ logger.setLogLevel(adjustConfig.logLevel);
+ }
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Event buffering is enabled");
+ }
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ if (playAdId == null) {
+ logger.info("Unable to get Google Play Services Advertising ID at start time");
+ }
+
+ if (adjustConfig.defaultTracker != null) {
+ logger.info("Default tracker: '%s'", adjustConfig.defaultTracker);
+ }
+
+ if (adjustConfig.referrer != null) {
+ sendReferrer(adjustConfig.referrer, adjustConfig.referrerClickTime); // send to background queue to make sure that activityState is valid
+ }
+
+ readAttribution();
+ readActivityState();
+
+ packageHandler = AdjustFactory.getPackageHandler(this, adjustConfig.context, toPause());
+
+ startInternal();
+ }
+
+ private void startInternal() {
+ // it shouldn't start if it was disabled after a first session
+ if (activityState != null
+ && !activityState.enabled) {
+ return;
+ }
+
+ updateStatusInternal();
+
+ processSession();
+
+ checkAttributionState();
+
+ startTimer();
+ }
+
+ private void processSession() {
+ long now = System.currentTimeMillis();
+
+ // very first session
+ if (activityState == null) {
+ activityState = new ActivityState();
+ activityState.sessionCount = 1; // this is the first session
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ activityState.enabled = this.enabled;
+ writeActivityState();
+ return;
+ }
+
+ long lastInterval = now - activityState.lastActivity;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ activityState.lastActivity = now;
+ writeActivityState();
+ return;
+ }
+
+ // new session
+ if (lastInterval > SESSION_INTERVAL) {
+ activityState.sessionCount++;
+ activityState.lastInterval = lastInterval;
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ writeActivityState();
+ return;
+ }
+
+ // new subsession
+ if (lastInterval > SUBSESSION_INTERVAL) {
+ activityState.subsessionCount++;
+ activityState.sessionLength += lastInterval;
+ activityState.lastActivity = now;
+ writeActivityState();
+ logger.info("Started subsession %d of session %d",
+ activityState.subsessionCount,
+ activityState.sessionCount);
+ }
+ }
+
+ private void checkAttributionState() {
+ // if there is no attribution saved, or there is one being asked
+ if (attribution == null || activityState.askingAttribution) {
+ getAttributionHandler().getAttribution();
+ }
+ }
+
+ private void endInternal() {
+ packageHandler.pauseSending();
+ getAttributionHandler().pauseSending();
+ stopTimer();
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void trackEventInternal(AdjustEvent event) {
+ if (!checkEvent(event)) return;
+ if (!activityState.enabled) return;
+
+ long now = System.currentTimeMillis();
+
+ activityState.eventCount++;
+ updateActivityState(now);
+
+ PackageBuilder eventBuilder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage eventPackage = eventBuilder.buildEventPackage(event);
+ packageHandler.addPackage(eventPackage);
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Buffered event %s", eventPackage.getSuffix());
+ } else {
+ packageHandler.sendFirstPackage();
+ }
+
+ writeActivityState();
+ }
+
+ private void finishedTrackingActivityInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ String deeplink = jsonResponse.optString("deeplink", null);
+ launchDeeplinkMain(deeplink);
+ getAttributionHandler().checkAttribution(jsonResponse);
+ }
+
+ private void sendReferrerInternal(String referrer, long clickTime) {
+ ActivityPackage clickPackage = buildQueryStringClickPackage(referrer,
+ "reftag",
+ clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private void readOpenUrlInternal(Uri url, long clickTime) {
+ if (url == null) {
+ return;
+ }
+
+ String queryString = url.getQuery();
+
+ ActivityPackage clickPackage = buildQueryStringClickPackage(queryString, "deeplink", clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private ActivityPackage buildQueryStringClickPackage(String queryString, String source, long clickTime) {
+ if (queryString == null) {
+ return null;
+ }
+
+ long now = System.currentTimeMillis();
+ Map<String, String> queryStringParameters = new HashMap<String, String>();
+ AdjustAttribution queryStringAttribution = new AdjustAttribution();
+ boolean hasAdjustTags = false;
+
+ String[] queryPairs = queryString.split("&");
+ for (String pair : queryPairs) {
+ if (readQueryString(pair, queryStringParameters, queryStringAttribution)) {
+ hasAdjustTags = true;
+ }
+ }
+
+ if (!hasAdjustTags) {
+ return null;
+ }
+
+ String reftag = queryStringParameters.remove("reftag");
+
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ builder.extraParameters = queryStringParameters;
+ builder.attribution = queryStringAttribution;
+ builder.reftag = reftag;
+ ActivityPackage clickPackage = builder.buildClickPackage(source, clickTime);
+ return clickPackage;
+ }
+
+ private boolean readQueryString(String queryString,
+ Map<String, String> extraParameters,
+ AdjustAttribution queryStringAttribution) {
+ String[] pairComponents = queryString.split("=");
+ if (pairComponents.length != 2) return false;
+
+ String key = pairComponents[0];
+ if (!key.startsWith(ADJUST_PREFIX)) return false;
+
+ String value = pairComponents[1];
+ if (value.length() == 0) return false;
+
+ String keyWOutPrefix = key.substring(ADJUST_PREFIX.length());
+ if (keyWOutPrefix.length() == 0) return false;
+
+ if (!trySetAttribution(queryStringAttribution, keyWOutPrefix, value)) {
+ extraParameters.put(keyWOutPrefix, value);
+ }
+
+ return true;
+ }
+
+ private boolean trySetAttribution(AdjustAttribution queryStringAttribution,
+ String key,
+ String value) {
+ if (key.equals("tracker")) {
+ queryStringAttribution.trackerName = value;
+ return true;
+ }
+
+ if (key.equals("campaign")) {
+ queryStringAttribution.campaign = value;
+ return true;
+ }
+
+ if (key.equals("adgroup")) {
+ queryStringAttribution.adgroup = value;
+ return true;
+ }
+
+ if (key.equals("creative")) {
+ queryStringAttribution.creative = value;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void updateStatusInternal() {
+ updateAttributionHandlerStatus();
+ updatePackageHandlerStatus();
+ }
+
+ private void updateAttributionHandlerStatus() {
+ if (attributionHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ attributionHandler.pauseSending();
+ } else {
+ attributionHandler.resumeSending();
+ }
+ }
+
+ private void updatePackageHandlerStatus() {
+ if (packageHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ packageHandler.pauseSending();
+ } else {
+ packageHandler.resumeSending();
+ }
+ }
+
+ private void launchDeeplinkMain(String deeplink) {
+ if (deeplink == null) return;
+
+ Uri location = Uri.parse(deeplink);
+ Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
+ mapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Verify it resolves
+ PackageManager packageManager = adjustConfig.context.getPackageManager();
+ List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0);
+ boolean isIntentSafe = activities.size() > 0;
+
+ // Start an activity if it's safe
+ if (!isIntentSafe) {
+ logger.error("Unable to open deep link (%s)", deeplink);
+ return;
+ }
+
+ logger.info("Open deep link (%s)", deeplink);
+ adjustConfig.context.startActivity(mapIntent);
+ }
+
+ private boolean updateActivityState(long now) {
+ long lastInterval = now - activityState.lastActivity;
+ // ignore late updates
+ if (lastInterval > SESSION_INTERVAL) {
+ return false;
+ }
+ activityState.lastActivity = now;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ } else {
+ activityState.sessionLength += lastInterval;
+ activityState.timeSpent += lastInterval;
+ }
+ return true;
+ }
+
+ public static boolean deleteActivityState(Context context) {
+ return context.deleteFile(ACTIVITY_STATE_FILENAME);
+ }
+
+ public static boolean deleteAttribution(Context context) {
+ return context.deleteFile(ATTRIBUTION_FILENAME);
+ }
+
+ private void transferSessionPackage(long now) {
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage sessionPackage = builder.buildSessionPackage();
+ packageHandler.addPackage(sessionPackage);
+ packageHandler.sendFirstPackage();
+ }
+
+ private void startTimer() {
+ stopTimer();
+
+ if (!activityState.enabled) {
+ return;
+ }
+ timer = Executors.newSingleThreadScheduledExecutor();
+ timer.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ timerFired();
+ }
+ }, TIMER_START, TIMER_INTERVAL, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopTimer() {
+ if (timer != null) {
+ timer.shutdown();
+ timer = null;
+ }
+ }
+
+ private void timerFired() {
+ if (!activityState.enabled) {
+ stopTimer();
+ return;
+ }
+
+ packageHandler.sendFirstPackage();
+
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void readActivityState() {
+ try {
+ /**
+ * Mozilla:
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void readAttribution() {
+ try {
+ /**
+ * Mozilla: (same as in readActivityState() )
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void writeActivityState() {
+ Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ }
+
+ private void writeAttribution() {
+ Util.writeObject(attribution, adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ }
+
+ private boolean checkEvent(AdjustEvent event) {
+ if (event == null) {
+ logger.error("Event missing");
+ return false;
+ }
+
+ if (!event.isValid()) {
+ logger.error("Event not initialized correctly");
+ return false;
+ }
+
+ return true;
+ }
+
+ // lazy initialization to prevent null activity state before first session
+ private IAttributionHandler getAttributionHandler() {
+ if (attributionHandler == null) {
+ ActivityPackage attributionPackage = getAttributionPackage();
+ attributionHandler = AdjustFactory.getAttributionHandler(this,
+ attributionPackage,
+ toPause());
+ }
+ return attributionHandler;
+ }
+
+ private boolean toPause() {
+ return offline || !isEnabled();
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
new file mode 100644
index 000000000..a255b83a9
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+public enum ActivityKind {
+ UNKNOWN, SESSION, EVENT, CLICK, ATTRIBUTION;
+
+ public static ActivityKind fromString(String string) {
+ if ("session".equals(string)) {
+ return SESSION;
+ } else if ("event".equals(string)) {
+ return EVENT;
+ } else if ("click".equals(string)) {
+ return CLICK;
+ } else if ("attribution".equals(string)) {
+ return ATTRIBUTION;
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SESSION:
+ return "session";
+ case EVENT:
+ return "event";
+ case CLICK:
+ return "click";
+ case ATTRIBUTION:
+ return "attribution";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
new file mode 100644
index 000000000..27ab969fd
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
@@ -0,0 +1,100 @@
+//
+// ActivityPackage.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class ActivityPackage implements Serializable {
+ private static final long serialVersionUID = -35935556512024097L;
+
+ // data
+ private String path;
+ private String clientSdk;
+ private Map<String, String> parameters;
+
+ // logs
+ private ActivityKind activityKind;
+ private String suffix;
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getClientSdk() {
+ return clientSdk;
+ }
+
+ public void setClientSdk(String clientSdk) {
+ this.clientSdk = clientSdk;
+ }
+
+ public Map<String, String> getParameters() {
+ return parameters;
+ }
+
+ public void setParameters(Map<String, String> parameters) {
+ this.parameters = parameters;
+ }
+
+ public ActivityKind getActivityKind() {
+ return activityKind;
+ }
+
+ public void setActivityKind(ActivityKind activityKind) {
+ this.activityKind = activityKind;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String toString() {
+ return String.format("%s%s", activityKind.toString(), suffix);
+ }
+
+ public String getExtendedString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format("Path: %s\n", path));
+ builder.append(String.format("ClientSdk: %s\n", clientSdk));
+
+ if (parameters != null) {
+ builder.append("Parameters:");
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ builder.append(String.format("\n\t%-16s %s", entry.getKey(), entry.getValue()));
+ }
+ }
+ return builder.toString();
+ }
+
+ protected String getSuccessMessage() {
+ try {
+ return String.format("Tracked %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Tracked ???";
+ }
+ }
+
+ protected String getFailureMessage() {
+ try {
+ return String.format("Failed to track %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Failed to track ???";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
new file mode 100644
index 000000000..41ad2ca3b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
@@ -0,0 +1,151 @@
+//
+// ActivityState.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectInputStream.GetField;
+import java.io.Serializable;
+import java.util.Calendar;
+import java.util.Locale;
+
+public class ActivityState implements Serializable, Cloneable {
+ private static final long serialVersionUID = 9039439291143138148L;
+ private transient String readErrorMessage = "Unable to read '%s' field in migration device with message (%s)";
+ private transient ILogger logger;
+
+ // persistent data
+ protected String uuid;
+ protected boolean enabled;
+ protected boolean askingAttribution;
+
+ // global counters
+ protected int eventCount;
+ protected int sessionCount;
+
+ // session attributes
+ protected int subsessionCount;
+ protected long sessionLength; // all durations in milliseconds
+ protected long timeSpent;
+ protected long lastActivity; // all times in milliseconds since 1970
+
+ protected long lastInterval;
+
+ protected ActivityState() {
+ logger = AdjustFactory.getLogger();
+ // create UUID for new devices
+ uuid = Util.createUuid();
+ enabled = true;
+ askingAttribution = false;
+
+ eventCount = 0; // no events yet
+ sessionCount = 0; // the first session just started
+ subsessionCount = -1; // we don't know how many subsessions this first session will have
+ sessionLength = -1; // same for session length and time spent
+ timeSpent = -1; // this information will be collected and attached to the next session
+ lastActivity = -1;
+ lastInterval = -1;
+ }
+
+ protected void resetSessionAttributes(long now) {
+ subsessionCount = 1; // first subsession
+ sessionLength = 0; // no session length yet
+ timeSpent = 0; // no time spent yet
+ lastActivity = now;
+ lastInterval = -1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "ec:%d sc:%d ssc:%d sl:%.1f ts:%.1f la:%s uuid:%s",
+ eventCount, sessionCount, subsessionCount,
+ sessionLength / 1000.0, timeSpent / 1000.0,
+ stamp(lastActivity), uuid);
+ }
+
+ @Override
+ public ActivityState clone() {
+ try {
+ return (ActivityState) super.clone();
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ GetField fields = stream.readFields();
+
+ eventCount = readIntField(fields, "eventCount", 0);
+ sessionCount = readIntField(fields, "sessionCount", 0);
+ subsessionCount = readIntField(fields, "subsessionCount", -1);
+ sessionLength = readLongField(fields, "sessionLength", -1l);
+ timeSpent = readLongField(fields, "timeSpent", -1l);
+ lastActivity = readLongField(fields, "lastActivity", -1l);
+ lastInterval = readLongField(fields, "lastInterval", -1l);
+
+ // new fields
+ uuid = readStringField(fields, "uuid", null);
+ enabled = readBooleanField(fields, "enabled", true);
+ askingAttribution = readBooleanField(fields, "askingAttribution", false);
+
+ // create UUID for migrating devices
+ if (uuid == null) {
+ uuid = Util.createUuid();
+ }
+ }
+
+ private String readStringField(GetField fields, String name, String defaultValue) {
+ try {
+ return (String) fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private boolean readBooleanField(GetField fields, String name, boolean defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private int readIntField(GetField fields, String name, int defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private long readLongField(GetField fields, String name, long defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private static String stamp(long dateMillis) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(dateMillis);
+ return String.format(Locale.US,
+ "%02d:%02d:%02d",
+ calendar.HOUR_OF_DAY,
+ calendar.MINUTE,
+ calendar.SECOND);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Adjust.java b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
new file mode 100644
index 000000000..3b81a077b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
@@ -0,0 +1,79 @@
+//
+// Adjust.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * The main interface to Adjust.
+ * Use the methods of this class to tell Adjust about the usage of your app.
+ * See the README for details.
+ */
+public class Adjust {
+
+ private static AdjustInstance defaultInstance;
+
+ private Adjust() {
+ }
+
+ public static synchronized AdjustInstance getDefaultInstance() {
+ if (defaultInstance == null) {
+ defaultInstance = new AdjustInstance();
+ }
+ return defaultInstance;
+ }
+
+ public static void onCreate(AdjustConfig adjustConfig) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onCreate(adjustConfig);
+ }
+
+ public static void trackEvent(AdjustEvent event) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.trackEvent(event);
+ }
+
+ public static void onResume() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onResume();
+ }
+
+ public static void onPause() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onPause();
+ }
+
+ public static void setEnabled(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setEnabled(enabled);
+ }
+
+ public static boolean isEnabled() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ return adjustInstance.isEnabled();
+ }
+
+ public static void appWillOpenUrl(Uri url) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.appWillOpenUrl(url);
+ }
+
+ public static void setReferrer(String referrer) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.sendReferrer(referrer);
+ }
+
+ public static void setOfflineMode(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setOfflineMode(enabled);
+ }
+}
+
+
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
new file mode 100644
index 000000000..4e3abb017
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
@@ -0,0 +1,62 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AdjustAttribution implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public String trackerToken;
+ public String trackerName;
+ public String network;
+ public String campaign;
+ public String adgroup;
+ public String creative;
+
+ public static AdjustAttribution fromJson(JSONObject jsonObject) {
+ if (jsonObject == null) return null;
+
+ AdjustAttribution attribution = new AdjustAttribution();
+
+ attribution.trackerToken = jsonObject.optString("tracker_token", null);
+ attribution.trackerName = jsonObject.optString("tracker_name", null);
+ attribution.network = jsonObject.optString("network", null);
+ attribution.campaign = jsonObject.optString("campaign", null);
+ attribution.adgroup = jsonObject.optString("adgroup", null);
+ attribution.creative = jsonObject.optString("creative", null);
+
+ return attribution;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if (other == null) return false;
+ if (getClass() != other.getClass()) return false;
+ AdjustAttribution otherAttribution = (AdjustAttribution) other;
+
+ if (!equalString(trackerToken, otherAttribution.trackerToken)) return false;
+ if (!equalString(trackerName, otherAttribution.trackerName)) return false;
+ if (!equalString(network, otherAttribution.network)) return false;
+ if (!equalString(campaign, otherAttribution.campaign)) return false;
+ if (!equalString(adgroup, otherAttribution.adgroup)) return false;
+ if (!equalString(creative, otherAttribution.creative)) return false;
+ return true;
+ }
+
+ private boolean equalString(String first, String second) {
+ if (first == null || second == null) {
+ return first == null && second == null;
+ }
+ return first.equals(second);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tt:%s tn:%s net:%s cam:%s adg:%s cre:%s",
+ trackerToken, trackerName, network, campaign, adgroup, creative);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
new file mode 100644
index 000000000..148a5f670
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
@@ -0,0 +1,128 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+public class AdjustConfig {
+ Context context;
+ String appToken;
+ String environment;
+ LogLevel logLevel;
+ String sdkPrefix;
+ Boolean eventBufferingEnabled;
+ String defaultTracker;
+ OnAttributionChangedListener onAttributionChangedListener;
+ String referrer;
+ long referrerClickTime;
+ Boolean knownDevice;
+
+ public static final String ENVIRONMENT_SANDBOX = "sandbox";
+ public static final String ENVIRONMENT_PRODUCTION = "production";
+
+ public AdjustConfig(Context context, String appToken, String environment) {
+ if (!isValid(context, appToken, environment)) {
+ return;
+ }
+
+ this.context = context.getApplicationContext();
+ this.appToken = appToken;
+ this.environment = environment;
+
+ // default values
+ this.logLevel = LogLevel.INFO;
+ this.eventBufferingEnabled = false;
+ }
+
+ public void setEventBufferingEnabled(Boolean eventBufferingEnabled) {
+ this.eventBufferingEnabled = eventBufferingEnabled;
+ }
+
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ public void setSdkPrefix(String sdkPrefix) {
+ this.sdkPrefix = sdkPrefix;
+ }
+
+ public void setDefaultTracker(String defaultTracker) {
+ this.defaultTracker = defaultTracker;
+ }
+
+ public void setOnAttributionChangedListener(OnAttributionChangedListener onAttributionChangedListener) {
+ this.onAttributionChangedListener = onAttributionChangedListener;
+ }
+
+ public boolean hasListener() {
+ return onAttributionChangedListener != null;
+ }
+
+ public boolean isValid() {
+ return appToken != null;
+ }
+
+ private boolean isValid(Context context, String appToken, String environment) {
+ if (!checkAppToken(appToken)) return false;
+ if (!checkEnvironment(environment)) return false;
+ if (!checkContext(context)) return false;
+
+ return true;
+ }
+
+ private static boolean checkContext(Context context) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (context == null) {
+ logger.error("Missing context");
+ return false;
+ }
+
+ if (!Util.checkPermission(context, android.Manifest.permission.INTERNET)) {
+ logger.error("Missing permission: INTERNET");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkAppToken(String appToken) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (appToken == null) {
+ logger.error("Missing App Token.");
+ return false;
+ }
+
+ if (appToken.length() != 12) {
+ logger.error("Malformed App Token '%s'", appToken);
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkEnvironment(String environment) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (environment == null) {
+ logger.error("Missing environment");
+ return false;
+ }
+
+ if (environment == AdjustConfig.ENVIRONMENT_SANDBOX) {
+ logger.Assert("SANDBOX: Adjust is running in Sandbox mode. " +
+ "Use this setting for testing. " +
+ "Don't forget to set the environment to `production` before publishing!");
+ return true;
+ }
+ if (environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.Assert(
+ "PRODUCTION: Adjust is running in Production mode. " +
+ "Use this setting only for the build that you want to publish. " +
+ "Set the environment to `sandbox` if you want to test your app!");
+ return true;
+ }
+
+ logger.error("Unknown environment '%s'", environment);
+ return false;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
new file mode 100644
index 000000000..f03718183
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
@@ -0,0 +1,112 @@
+package com.adjust.sdk;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by pfms on 05/11/14.
+ */
+public class AdjustEvent {
+ String eventToken;
+ Double revenue;
+ String currency;
+ Map<String, String> callbackParameters;
+ Map<String, String> partnerParameters;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public AdjustEvent(String eventToken) {
+ if (!checkEventToken(eventToken, logger)) return;
+
+ this.eventToken = eventToken;
+ }
+
+ public void setRevenue(double revenue, String currency) {
+ if (!checkRevenue(revenue, currency)) return;
+
+ this.revenue = revenue;
+ this.currency = currency;
+ }
+
+ public void addCallbackParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Callback")) return;
+ if (!isValidParameter(value, "value", "Callback")) return;
+
+ if (callbackParameters == null) {
+ callbackParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = callbackParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public void addPartnerParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Partner")) return;
+ if (!isValidParameter(value, "value", "Partner")) return;
+
+ if (partnerParameters == null) {
+ partnerParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = partnerParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public boolean isValid() {
+ return eventToken != null;
+ }
+
+ private static boolean checkEventToken(String eventToken, ILogger logger) {
+ if (eventToken == null) {
+ logger.error("Missing Event Token");
+ return false;
+ }
+ if (eventToken.length() != 6) {
+ logger.error("Malformed Event Token '%s'", eventToken);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkRevenue(Double revenue, String currency) {
+ if (revenue != null) {
+ if (revenue < 0.0) {
+ logger.error("Invalid amount %.4f", revenue);
+ return false;
+ }
+
+ if (currency == null) {
+ logger.error("Currency must be set with revenue");
+ return false;
+ }
+ if (currency == "") {
+ logger.error("Currency is empty");
+ return false;
+ }
+
+ } else if (currency != null) {
+ logger.error("Revenue must be set with currency");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidParameter(String attribute, String attributeType, String parameterName) {
+ if (attribute == null) {
+ logger.error("%s parameter %s is missing", parameterName, attributeType);
+ return false;
+ }
+ if (attribute == "") {
+ logger.error("%s parameter %s is empty", parameterName, attributeType);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
new file mode 100644
index 000000000..802af6416
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
@@ -0,0 +1,141 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+public class AdjustFactory {
+ private static IPackageHandler packageHandler = null;
+ private static IRequestHandler requestHandler = null;
+ private static IAttributionHandler attributionHandler = null;
+ private static IActivityHandler activityHandler = null;
+ private static ILogger logger = null;
+ private static HttpClient httpClient = null;
+
+ private static long timerInterval = -1;
+ private static long timerStart = -1;
+ private static long sessionInterval = -1;
+ private static long subsessionInterval = -1;
+
+ public static IPackageHandler getPackageHandler(ActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ if (packageHandler == null) {
+ return new PackageHandler(activityHandler, context, startPaused);
+ }
+ packageHandler.init(activityHandler, context, startPaused);
+ return packageHandler;
+ }
+
+ public static IRequestHandler getRequestHandler(IPackageHandler packageHandler) {
+ if (requestHandler == null) {
+ return new RequestHandler(packageHandler);
+ }
+ requestHandler.init(packageHandler);
+ return requestHandler;
+ }
+
+ public static ILogger getLogger() {
+ if (logger == null) {
+ // Logger needs to be "static" to retain the configuration throughout the app
+ logger = new Logger();
+ }
+ return logger;
+ }
+
+ public static HttpClient getHttpClient(HttpParams params) {
+ if (httpClient == null) {
+ return new DefaultHttpClient(params);
+ }
+ return httpClient;
+ }
+
+ public static long getTimerInterval() {
+ if (timerInterval == -1) {
+ return Constants.ONE_MINUTE;
+ }
+ return timerInterval;
+ }
+
+ public static long getTimerStart() {
+ if (timerStart == -1) {
+ return 0;
+ }
+ return timerStart;
+ }
+
+ public static long getSessionInterval() {
+ if (sessionInterval == -1) {
+ return Constants.THIRTY_MINUTES;
+ }
+ return sessionInterval;
+ }
+
+ public static long getSubsessionInterval() {
+ if (subsessionInterval == -1) {
+ return Constants.ONE_SECOND;
+ }
+ return subsessionInterval;
+ }
+
+ public static IActivityHandler getActivityHandler(AdjustConfig config) {
+ if (activityHandler == null) {
+ return ActivityHandler.getInstance(config);
+ }
+ activityHandler.init(config);
+ return activityHandler;
+ }
+
+ public static IAttributionHandler getAttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ if (attributionHandler == null) {
+ return new AttributionHandler(activityHandler, attributionPackage, startPaused);
+ }
+ attributionHandler.init(activityHandler, attributionPackage, startPaused);
+ return attributionHandler;
+ }
+
+ public static void setPackageHandler(IPackageHandler packageHandler) {
+ AdjustFactory.packageHandler = packageHandler;
+ }
+
+ public static void setRequestHandler(IRequestHandler requestHandler) {
+ AdjustFactory.requestHandler = requestHandler;
+ }
+
+ public static void setLogger(ILogger logger) {
+ AdjustFactory.logger = logger;
+ }
+
+ public static void setHttpClient(HttpClient httpClient) {
+ AdjustFactory.httpClient = httpClient;
+ }
+
+ public static void setTimerInterval(long timerInterval) {
+ AdjustFactory.timerInterval = timerInterval;
+ }
+
+ public static void setTimerStart(long timerStart) {
+ AdjustFactory.timerStart = timerStart;
+ }
+
+ public static void setSessionInterval(long sessionInterval) {
+ AdjustFactory.sessionInterval = sessionInterval;
+ }
+
+ public static void setSubsessionInterval(long subsessionInterval) {
+ AdjustFactory.subsessionInterval = subsessionInterval;
+ }
+
+ public static void setActivityHandler(IActivityHandler activityHandler) {
+ AdjustFactory.activityHandler = activityHandler;
+ }
+
+ public static void setAttributionHandler(IAttributionHandler attributionHandler) {
+ AdjustFactory.attributionHandler = attributionHandler;
+ }
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
new file mode 100644
index 000000000..158fb7ca1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
@@ -0,0 +1,86 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * Created by pfms on 04/12/14.
+ */
+public class AdjustInstance {
+
+ private String referrer;
+ private long referrerClickTime;
+ private ActivityHandler activityHandler;
+
+ private static ILogger getLogger() {
+ return AdjustFactory.getLogger();
+ }
+
+ public void onCreate(AdjustConfig adjustConfig) {
+ if (activityHandler != null) {
+ getLogger().error("Adjust already initialized");
+ return;
+ }
+
+ adjustConfig.referrer = this.referrer;
+ adjustConfig.referrerClickTime = this.referrerClickTime;
+
+ activityHandler = ActivityHandler.getInstance(adjustConfig);
+ }
+
+ public void trackEvent(AdjustEvent event) {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackEvent(event);
+ }
+
+ public void onResume() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionStart();
+ }
+
+ public void onPause() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionEnd();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setEnabled(enabled);
+ }
+
+ public boolean isEnabled() {
+ if (!checkActivityHandler()) return false;
+ return activityHandler.isEnabled();
+ }
+
+ public void appWillOpenUrl(Uri url) {
+ if (!checkActivityHandler()) return;
+ long clickTime = System.currentTimeMillis();
+ activityHandler.readOpenUrl(url, clickTime);
+ }
+
+ public void sendReferrer(String referrer) {
+ long clickTime = System.currentTimeMillis();
+ // sendReferrer might be triggered before Adjust
+ if (activityHandler == null) {
+ // save it to inject in the config before launch
+ this.referrer = referrer;
+ this.referrerClickTime = clickTime;
+ } else {
+ activityHandler.sendReferrer(referrer, clickTime);
+ }
+ }
+
+ public void setOfflineMode(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setOfflineMode(enabled);
+ }
+
+ private boolean checkActivityHandler() {
+ if (activityHandler == null) {
+ getLogger().error("Please initialize Adjust by calling 'onCreate' before");
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
new file mode 100644
index 000000000..cfeecd8d0
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.MALFORMED;
+import static com.adjust.sdk.Constants.REFERRER;
+
+// support multiple BroadcastReceivers for the INSTALL_REFERRER:
+// http://blog.appington.com/2012/08/01/giving-credit-for-android-app-installs
+
+public class AdjustReferrerReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String rawReferrer = intent.getStringExtra(REFERRER);
+ if (null == rawReferrer) {
+ return;
+ }
+
+ String referrer;
+ try {
+ referrer = URLDecoder.decode(rawReferrer, ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ referrer = MALFORMED;
+ }
+
+ AdjustInstance adjust = Adjust.getDefaultInstance();
+ adjust.sendReferrer(referrer);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
new file mode 100644
index 000000000..0d550a83a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
@@ -0,0 +1,155 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AttributionHandler implements IAttributionHandler {
+ private ScheduledExecutorService scheduler;
+ private IActivityHandler activityHandler;
+ private ILogger logger;
+ private ActivityPackage attributionPackage;
+ private ScheduledFuture waitingTask;
+ private HttpClient httpClient;
+ private boolean paused;
+
+ public AttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ logger = AdjustFactory.getLogger();
+ httpClient = Util.getHttpClient();
+ init(activityHandler, attributionPackage, startPaused);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.attributionPackage = attributionPackage;
+ this.paused = startPaused;
+ }
+
+ @Override
+ public void getAttribution() {
+ getAttribution(0);
+ }
+
+ @Override
+ public void checkAttribution(final JSONObject jsonResponse) {
+ scheduler.submit(new Runnable() {
+ @Override
+ public void run() {
+ checkAttributionInternal(jsonResponse);
+ }
+ });
+ }
+
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ private void getAttribution(int delayInMilliseconds) {
+ if (waitingTask != null) {
+ waitingTask.cancel(false);
+ }
+
+ if (delayInMilliseconds != 0) {
+ logger.debug("Waiting to query attribution in %d milliseconds", delayInMilliseconds);
+ }
+
+ waitingTask = scheduler.schedule(new Runnable() {
+ @Override
+ public void run() {
+ getAttributionInternal();
+ }
+ }, delayInMilliseconds, TimeUnit.MILLISECONDS);
+ }
+
+ private void checkAttributionInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) return;
+
+ JSONObject attributionJson = jsonResponse.optJSONObject("attribution");
+ AdjustAttribution attribution = AdjustAttribution.fromJson(attributionJson);
+
+ int timerMilliseconds = jsonResponse.optInt("ask_in", -1);
+
+ // without ask_in attribute
+ if (timerMilliseconds < 0) {
+ activityHandler.tryUpdateAttribution(attribution);
+
+ activityHandler.setAskingAttribution(false);
+
+ return;
+ }
+
+ activityHandler.setAskingAttribution(true);
+
+ getAttribution(timerMilliseconds);
+ }
+
+ private void getAttributionInternal() {
+ if (paused) {
+ logger.debug("Attribution Handler is paused");
+ return;
+ }
+ logger.verbose("%s", attributionPackage.getExtendedString());
+ HttpResponse httpResponse = null;
+ try {
+ HttpGet request = getRequest(attributionPackage);
+ httpResponse = httpClient.execute(request);
+ } catch (Exception e) {
+ logger.error("Failed to get attribution (%s)", e.getMessage());
+ return;
+ }
+
+ JSONObject jsonResponse = Util.parseJsonResponse(httpResponse, logger);
+
+ checkAttributionInternal(jsonResponse);
+ }
+
+ private Uri buildUri(ActivityPackage attributionPackage) {
+ Uri.Builder uriBuilder = new Uri.Builder();
+
+ uriBuilder.scheme(Constants.SCHEME);
+ uriBuilder.authority(Constants.AUTHORITY);
+ uriBuilder.appendPath(attributionPackage.getPath());
+
+ for (Map.Entry<String, String> entry : attributionPackage.getParameters().entrySet()) {
+ uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
+ }
+
+ return uriBuilder.build();
+ }
+
+ private HttpGet getRequest(ActivityPackage attributionPackage) throws URISyntaxException {
+ HttpGet request = new HttpGet();
+ Uri uri = buildUri(attributionPackage);
+ request.setURI(new URI(uri.toString()));
+
+ request.addHeader("Client-SDK", attributionPackage.getClientSdk());
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Constants.java b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
new file mode 100644
index 000000000..7a97cb2f4
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
@@ -0,0 +1,53 @@
+//
+// Constants.java
+// Adjust
+//
+// Created by keyboardsurfer on 2013-11-08.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author keyboardsurfer
+ * @since 8.11.13
+ */
+public interface Constants {
+ int ONE_SECOND = 1000;
+ int ONE_MINUTE = 60 * ONE_SECOND;
+ int THIRTY_MINUTES = 30 * ONE_MINUTE;
+
+ int CONNECTION_TIMEOUT = Constants.ONE_MINUTE;
+ int SOCKET_TIMEOUT = Constants.ONE_MINUTE;
+
+ String BASE_URL = "https://app.adjust.com";
+ String SCHEME = "https";
+ String AUTHORITY = "app.adjust.com";
+ String CLIENT_SDK = "android4.0.0";
+ String LOGTAG = "Adjust";
+
+ String ACTIVITY_STATE_FILENAME = "AdjustIoActivityState";
+ String ATTRIBUTION_FILENAME = "AdjustAttribution";
+
+ String MALFORMED = "malformed";
+ String SMALL = "small";
+ String NORMAL = "normal";
+ String LONG = "long";
+ String LARGE = "large";
+ String XLARGE = "xlarge";
+ String LOW = "low";
+ String MEDIUM = "medium";
+ String HIGH = "high";
+ String REFERRER = "referrer";
+
+ String ENCODING = "UTF-8";
+ String MD5 = "MD5";
+ String SHA1 = "SHA-1";
+
+ // List of known plugins, possibly not active
+ List<String> PLUGINS = Arrays.asList();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
new file mode 100644
index 000000000..5cccb77f4
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
@@ -0,0 +1,290 @@
+package com.adjust.sdk;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.HIGH;
+import static com.adjust.sdk.Constants.LARGE;
+import static com.adjust.sdk.Constants.LONG;
+import static com.adjust.sdk.Constants.LOW;
+import static com.adjust.sdk.Constants.MD5;
+import static com.adjust.sdk.Constants.MEDIUM;
+import static com.adjust.sdk.Constants.NORMAL;
+import static com.adjust.sdk.Constants.SHA1;
+import static com.adjust.sdk.Constants.SMALL;
+import static com.adjust.sdk.Constants.XLARGE;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+class DeviceInfo {
+ String macSha1;
+ String macShortMd5;
+ String androidId;
+ String fbAttributionId;
+ String clientSdk;
+ String packageName;
+ String appVersion;
+ String deviceType;
+ String deviceName;
+ String deviceManufacturer;
+ String osName;
+ String osVersion;
+ String language;
+ String country;
+ String screenSize;
+ String screenFormat;
+ String screenDensity;
+ String displayWidth;
+ String displayHeight;
+ Map<String, String> pluginKeys;
+
+ DeviceInfo(Context context, String sdkPrefix) {
+ Resources resources = context.getResources();
+ DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+ Configuration configuration = resources.getConfiguration();
+ Locale locale = configuration.locale;
+ int screenLayout = configuration.screenLayout;
+ boolean isGooglePlayServicesAvailable = Reflection.isGooglePlayServicesAvailable(context);
+ String macAddress = getMacAddress(context, isGooglePlayServicesAvailable);
+
+ packageName = getPackageName(context);
+ appVersion = getAppVersion(context);
+ deviceType = getDeviceType(screenLayout);
+ deviceName = getDeviceName();
+ deviceManufacturer = getDeviceManufacturer();
+ osName = getOsName();
+ osVersion = getOsVersion();
+ language = getLanguage(locale);
+ country = getCountry(locale);
+ screenSize = getScreenSize(screenLayout);
+ screenFormat = getScreenFormat(screenLayout);
+ screenDensity = getScreenDensity(displayMetrics);
+ displayWidth = getDisplayWidth(displayMetrics);
+ displayHeight = getDisplayHeight(displayMetrics);
+ clientSdk = getClientSdk(sdkPrefix);
+ androidId = getAndroidId(context, isGooglePlayServicesAvailable);
+ fbAttributionId = getFacebookAttributionId(context);
+ pluginKeys = Reflection.getPluginKeys(context);
+ macSha1 = getMacSha1(macAddress);
+ macShortMd5 = getMacShortMd5(macAddress);
+ }
+
+ private String getMacAddress(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ if (!!Util.checkPermission(context, android.Manifest.permission.ACCESS_WIFI_STATE)) {
+ AdjustFactory.getLogger().warn("Missing permission: ACCESS_WIFI_STATE");
+ }
+ return Reflection.getMacAddress(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String getPackageName(Context context) {
+ return context.getPackageName();
+ }
+
+ private String getAppVersion(Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ String name = context.getPackageName();
+ PackageInfo info = packageManager.getPackageInfo(name, 0);
+ return info.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private String getDeviceType(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return "phone";
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ case 4:
+ return "tablet";
+ default:
+ return null;
+ }
+ }
+
+ private String getDeviceName() {
+ return Build.MODEL;
+ }
+
+ private String getDeviceManufacturer() {
+ return Build.MANUFACTURER;
+ }
+
+ private String getOsName() {
+ return "android";
+ }
+
+ private String getOsVersion() {
+ return osVersion = "" + Build.VERSION.SDK_INT;
+ }
+
+ private String getLanguage(Locale locale) {
+ return locale.getLanguage();
+ }
+
+ private String getCountry(Locale locale) {
+ return locale.getCountry();
+ }
+
+ private String getScreenSize(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ return SMALL;
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return NORMAL;
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ return LARGE;
+ case 4:
+ return XLARGE;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenFormat(int screenLayout) {
+ int screenFormat = screenLayout & Configuration.SCREENLAYOUT_LONG_MASK;
+
+ switch (screenFormat) {
+ case Configuration.SCREENLAYOUT_LONG_YES:
+ return LONG;
+ case Configuration.SCREENLAYOUT_LONG_NO:
+ return NORMAL;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenDensity(DisplayMetrics displayMetrics) {
+ int density = displayMetrics.densityDpi;
+ int low = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_LOW) / 2;
+ int high = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_HIGH) / 2;
+
+ if (0 == density) {
+ return null;
+ } else if (density < low) {
+ return LOW;
+ } else if (density > high) {
+ return HIGH;
+ }
+ return MEDIUM;
+ }
+
+ private String getDisplayWidth(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.widthPixels);
+ }
+
+ private String getDisplayHeight(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.heightPixels);
+ }
+
+ private String getClientSdk(String sdkPrefix) {
+ if (sdkPrefix == null) {
+ return Constants.CLIENT_SDK;
+ } else {
+ return String.format("%s@%s", sdkPrefix, Constants.CLIENT_SDK);
+ }
+ }
+
+ private String getMacSha1(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macSha1 = sha1(macAddress);
+
+ return macSha1;
+ }
+
+ private String getMacShortMd5(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macShort = macAddress.replaceAll(":", "");
+ String macShortMd5 = md5(macShort);
+
+ return macShortMd5;
+ }
+
+ private String getAndroidId(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ return Reflection.getAndroidId(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String sha1(final String text) {
+ return hash(text, SHA1);
+ }
+
+ private String md5(final String text) {
+ return hash(text, MD5);
+ }
+
+ private String hash(final String text, final String method) {
+ String hashString = null;
+ try {
+ final byte[] bytes = text.getBytes(ENCODING);
+ final MessageDigest mesd = MessageDigest.getInstance(method);
+ mesd.update(bytes, 0, bytes.length);
+ final byte[] hash = mesd.digest();
+ hashString = convertToHex(hash);
+ } catch (Exception e) {
+ }
+ return hashString;
+ }
+
+ private static String convertToHex(final byte[] bytes) {
+ final BigInteger bigInt = new BigInteger(1, bytes);
+ final String formatString = "%0" + (bytes.length << 1) + "x";
+ return String.format(formatString, bigInt);
+ }
+
+ private String getFacebookAttributionId(final Context context) {
+ try {
+ final ContentResolver contentResolver = context.getContentResolver();
+ final Uri uri = Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider");
+ final String columnName = "aid";
+ final String[] projection = {columnName};
+ final Cursor cursor = contentResolver.query(uri, projection, null, null, null);
+
+ if (null == cursor) {
+ return null;
+ }
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return null;
+ }
+
+ final String attributionId = cursor.getString(cursor.getColumnIndex(columnName));
+ cursor.close();
+ return attributionId;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
new file mode 100644
index 000000000..10b92205d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
@@ -0,0 +1,36 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IActivityHandler {
+ public void init(AdjustConfig config);
+
+ public void trackSubsessionStart();
+
+ public void trackSubsessionEnd();
+
+ public void trackEvent(AdjustEvent event);
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void setEnabled(boolean enabled);
+
+ public boolean isEnabled();
+
+ public void readOpenUrl(Uri url, long clickTime);
+
+ public boolean tryUpdateAttribution(AdjustAttribution attribution);
+
+ public void sendReferrer(String referrer, long clickTime);
+
+ public void setOfflineMode(boolean enabled);
+
+ public void setAskingAttribution(boolean askingAttribution);
+
+ public ActivityPackage getAttributionPackage();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
new file mode 100644
index 000000000..d4e701f75
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IAttributionHandler {
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused);
+
+ public void getAttribution();
+
+ public void checkAttribution(JSONObject jsonResponse);
+
+ public void pauseSending();
+
+ public void resumeSending();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ILogger.java b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
new file mode 100644
index 000000000..28f92af4b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+public interface ILogger {
+ public void setLogLevel(LogLevel logLevel);
+
+ public void setLogLevelString(String logLevelString);
+
+ public void verbose(String message, Object... parameters);
+
+ public void debug(String message, Object... parameters);
+
+ public void info(String message, Object... parameters);
+
+ public void warn(String message, Object... parameters);
+
+ public void error(String message, Object... parameters);
+
+ public void Assert(String message, Object... parameters);
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
new file mode 100644
index 000000000..99c300364
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
@@ -0,0 +1,27 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+public interface IPackageHandler {
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused);
+
+ public void addPackage(ActivityPackage pack);
+
+ public void sendFirstPackage();
+
+ public void sendNextPackage();
+
+ public void closeFirstPackage();
+
+ public void pauseSending();
+
+ public void resumeSending();
+
+ public String getFailureMessage();
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
new file mode 100644
index 000000000..5b18e2ee9
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
@@ -0,0 +1,9 @@
+package com.adjust.sdk;
+
+public interface IRequestHandler {
+ public void init(IPackageHandler packageHandler);
+
+ public void sendPackage(ActivityPackage pack);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LICENSE b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
new file mode 100644
index 000000000..25e1d5eb5
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2012-2014 adjust GmbH,
+http://www.adjust.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
new file mode 100644
index 000000000..5c0b410c2
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
@@ -0,0 +1,19 @@
+package com.adjust.sdk;
+
+import android.util.Log;
+
+/**
+ * Created by pfms on 11/03/15.
+ */
+public enum LogLevel {
+ VERBOSE(Log.VERBOSE), DEBUG(Log.DEBUG), INFO(Log.INFO), WARN(Log.WARN), ERROR(Log.ERROR), ASSERT(Log.ASSERT);
+ final int androidLogLevel;
+
+ LogLevel(final int androidLogLevel) {
+ this.androidLogLevel = androidLogLevel;
+ }
+
+ public int getAndroidLogLevel() {
+ return androidLogLevel;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Logger.java b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
new file mode 100644
index 000000000..86a644d4a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
@@ -0,0 +1,107 @@
+//
+// Logger.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-04-18.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class Logger implements ILogger {
+
+ private LogLevel logLevel;
+ private static String formatErrorMessage = "Error formating log message: %s, with params: %s";
+
+ public Logger() {
+ setLogLevel(LogLevel.INFO);
+ }
+
+ @Override
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public void setLogLevelString(String logLevelString) {
+ if (null != logLevelString) {
+ try {
+ setLogLevel(LogLevel.valueOf(logLevelString.toUpperCase(Locale.US)));
+ } catch (IllegalArgumentException iae) {
+ error("Malformed logLevel '%s', falling back to 'info'", logLevelString);
+ }
+ }
+ }
+
+ @Override
+ public void verbose(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.VERBOSE) {
+ try {
+ Log.v(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void debug(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.DEBUG) {
+ try {
+ Log.d(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void info(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.INFO) {
+ try {
+ Log.i(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void warn(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.WARN) {
+ try {
+ Log.w(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void error(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.ERROR) {
+ try {
+ Log.e(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void Assert(String message, Object... parameters) {
+ try {
+ Log.println(Log.ASSERT, LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
new file mode 100644
index 000000000..137d50d4d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
@@ -0,0 +1,5 @@
+package com.adjust.sdk;
+
+public interface OnAttributionChangedListener {
+ public void onAttributionChanged(AdjustAttribution attribution);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
new file mode 100644
index 000000000..3a43045fd
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
@@ -0,0 +1,291 @@
+//
+// PackageBuilder.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+class PackageBuilder {
+ private AdjustConfig adjustConfig;
+ private DeviceInfo deviceInfo;
+ private ActivityState activityState;
+ private long createdAt;
+
+ // reattributions
+ Map<String, String> extraParameters;
+ AdjustAttribution attribution;
+ String reftag;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public PackageBuilder(AdjustConfig adjustConfig,
+ DeviceInfo deviceInfo,
+ ActivityState activityState,
+ long createdAt) {
+ this.adjustConfig = adjustConfig;
+ this.deviceInfo = deviceInfo;
+ this.activityState = activityState.clone();
+ this.createdAt = createdAt;
+ }
+
+ public ActivityPackage buildSessionPackage() {
+ Map<String, String> parameters = getDefaultParameters();
+ addDuration(parameters, "last_interval", activityState.lastInterval);
+ addString(parameters, "default_tracker", adjustConfig.defaultTracker);
+
+ ActivityPackage sessionPackage = getDefaultActivityPackage();
+ sessionPackage.setPath("/session");
+ sessionPackage.setActivityKind(ActivityKind.SESSION);
+ sessionPackage.setSuffix("");
+ sessionPackage.setParameters(parameters);
+
+ return sessionPackage;
+ }
+
+ public ActivityPackage buildEventPackage(AdjustEvent event) {
+ Map<String, String> parameters = getDefaultParameters();
+ addInt(parameters, "event_count", activityState.eventCount);
+ addString(parameters, "event_token", event.eventToken);
+ addDouble(parameters, "revenue", event.revenue);
+ addString(parameters, "currency", event.currency);
+ addMapJson(parameters, "callback_params", event.callbackParameters);
+ addMapJson(parameters, "partner_params", event.partnerParameters);
+
+ ActivityPackage eventPackage = getDefaultActivityPackage();
+ eventPackage.setPath("/event");
+ eventPackage.setActivityKind(ActivityKind.EVENT);
+ eventPackage.setSuffix(getEventSuffix(event));
+ eventPackage.setParameters(parameters);
+
+ return eventPackage;
+ }
+
+ public ActivityPackage buildClickPackage(String source, long clickTime) {
+ Map<String, String> parameters = getDefaultParameters();
+
+ addString(parameters, "source", source);
+ addDate(parameters, "click_time", clickTime);
+ addString(parameters, "reftag", reftag);
+ addMapJson(parameters, "params", extraParameters);
+ injectAttribution(parameters);
+
+ ActivityPackage clickPackage = getDefaultActivityPackage();
+ clickPackage.setPath("/sdk_click");
+ clickPackage.setActivityKind(ActivityKind.CLICK);
+ clickPackage.setSuffix("");
+ clickPackage.setParameters(parameters);
+
+ return clickPackage;
+ }
+
+ public ActivityPackage buildAttributionPackage() {
+ Map<String, String> parameters = getIdsParameters();
+
+ ActivityPackage attributionPackage = getDefaultActivityPackage();
+ attributionPackage.setPath("attribution"); // does not contain '/' because of Uri.Builder.appendPath
+ attributionPackage.setActivityKind(ActivityKind.ATTRIBUTION);
+ attributionPackage.setSuffix("");
+ attributionPackage.setParameters(parameters);
+
+ return attributionPackage;
+ }
+
+ private ActivityPackage getDefaultActivityPackage() {
+ ActivityPackage activityPackage = new ActivityPackage();
+ activityPackage.setClientSdk(deviceInfo.clientSdk);
+ return activityPackage;
+ }
+
+ private Map<String, String> getDefaultParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfo(parameters);
+ injectConfig(parameters);
+ injectActivityState(parameters);
+ addDate(parameters, "created_at", createdAt);
+
+ // general
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private Map<String, String> getIdsParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfoIds(parameters);
+ injectConfig(parameters);
+ injectActivityStateIds(parameters);
+
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private void injectDeviceInfo(Map<String, String> parameters) {
+ injectDeviceInfoIds(parameters);
+ addString(parameters, "fb_id", deviceInfo.fbAttributionId);
+ addString(parameters, "package_name", deviceInfo.packageName);
+ addString(parameters, "app_version", deviceInfo.appVersion);
+ addString(parameters, "device_type", deviceInfo.deviceType);
+ addString(parameters, "device_name", deviceInfo.deviceName);
+ addString(parameters, "device_manufacturer", deviceInfo.deviceManufacturer);
+ addString(parameters, "os_name", deviceInfo.osName);
+ addString(parameters, "os_version", deviceInfo.osVersion);
+ addString(parameters, "language", deviceInfo.language);
+ addString(parameters, "country", deviceInfo.country);
+ addString(parameters, "screen_size", deviceInfo.screenSize);
+ addString(parameters, "screen_format", deviceInfo.screenFormat);
+ addString(parameters, "screen_density", deviceInfo.screenDensity);
+ addString(parameters, "display_width", deviceInfo.displayWidth);
+ addString(parameters, "display_height", deviceInfo.displayHeight);
+ fillPluginKeys(parameters);
+ }
+
+ private void injectDeviceInfoIds(Map<String, String> parameters) {
+ addString(parameters, "mac_sha1", deviceInfo.macSha1);
+ addString(parameters, "mac_md5", deviceInfo.macShortMd5);
+ addString(parameters, "android_id", deviceInfo.androidId);
+ }
+
+ private void injectConfig(Map<String, String> parameters) {
+ addString(parameters, "app_token", adjustConfig.appToken);
+ addString(parameters, "environment", adjustConfig.environment);
+ addBoolean(parameters, "device_known", adjustConfig.knownDevice);
+ addBoolean(parameters, "needs_attribution_data", adjustConfig.hasListener());
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ addString(parameters, "gps_adid", playAdId);
+ Boolean isTrackingEnabled = Util.isPlayTrackingEnabled(adjustConfig.context);
+ addBoolean(parameters, "tracking_enabled", isTrackingEnabled);
+ }
+
+ private void injectActivityState(Map<String, String> parameters) {
+ injectActivityStateIds(parameters);
+ addInt(parameters, "session_count", activityState.sessionCount);
+ addInt(parameters, "subsession_count", activityState.subsessionCount);
+ addDuration(parameters, "session_length", activityState.sessionLength);
+ addDuration(parameters, "time_spent", activityState.timeSpent);
+ }
+
+ private void injectActivityStateIds(Map<String, String> parameters) {
+ addString(parameters, "android_uuid", activityState.uuid);
+ }
+
+ private void injectAttribution(Map<String, String> parameters) {
+ if (attribution == null) {
+ return;
+ }
+ addString(parameters, "tracker", attribution.trackerName);
+ addString(parameters, "campaign", attribution.campaign);
+ addString(parameters, "adgroup", attribution.adgroup);
+ addString(parameters, "creative", attribution.creative);
+ }
+
+ private void checkDeviceIds(Map<String, String> parameters) {
+ if (!parameters.containsKey("mac_sha1")
+ && !parameters.containsKey("mac_md5")
+ && !parameters.containsKey("android_id")
+ && !parameters.containsKey("gps_adid")) {
+ logger.error("Missing device id's. Please check if Proguard is correctly set with Adjust SDK");
+ }
+ }
+
+ private void fillPluginKeys(Map<String, String> parameters) {
+ if (deviceInfo.pluginKeys == null) {
+ return;
+ }
+
+ for (Map.Entry<String, String> entry : deviceInfo.pluginKeys.entrySet()) {
+ addString(parameters, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private String getEventSuffix(AdjustEvent event) {
+ if (event.revenue == null) {
+ return String.format(" '%s'", event.eventToken);
+ } else {
+ return String.format(Locale.US, " (%.4f %s, '%s')", event.revenue, event.currency, event.eventToken);
+ }
+ }
+
+ private void addString(Map<String, String> parameters, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ return;
+ }
+
+ parameters.put(key, value);
+ }
+
+ private void addInt(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String valueString = Long.toString(value);
+ addString(parameters, key, valueString);
+ }
+
+ private void addDate(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String dateString = Util.dateFormat(value);
+ addString(parameters, key, dateString);
+ }
+
+ private void addDuration(Map<String, String> parameters, String key, long durationInMilliSeconds) {
+ if (durationInMilliSeconds < 0) {
+ return;
+ }
+
+ long durationInSeconds = (durationInMilliSeconds + 500) / 1000;
+ addInt(parameters, key, durationInSeconds);
+ }
+
+ private void addMapJson(Map<String, String> parameters, String key, Map<String, String> map) {
+ if (map == null) {
+ return;
+ }
+
+ if (map.size() == 0) {
+ return;
+ }
+
+ JSONObject jsonObject = new JSONObject(map);
+ String jsonString = jsonObject.toString();
+
+ addString(parameters, key, jsonString);
+ }
+
+ private void addBoolean(Map<String, String> parameters, String key, Boolean value) {
+ if (value == null) {
+ return;
+ }
+
+ int intValue = value ? 1 : 0;
+
+ addInt(parameters, key, intValue);
+ }
+
+ private void addDouble(Map<String, String> parameters, String key, Double value) {
+ if (value == null) return;
+
+ String doubleString = String.format("%.5f", value);
+
+ addString(parameters, key, doubleString);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
new file mode 100644
index 000000000..d0a84ccd1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
@@ -0,0 +1,274 @@
+//
+// PackageHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// persistent
+public class PackageHandler extends HandlerThread implements IPackageHandler {
+ private static final String PACKAGE_QUEUE_FILENAME = "AdjustIoPackageQueue";
+
+ private final InternalHandler internalHandler;
+ private IRequestHandler requestHandler;
+ private IActivityHandler activityHandler;
+ private List<ActivityPackage> packageQueue;
+ private AtomicBoolean isSending;
+ private boolean paused;
+ private Context context;
+ private ILogger logger;
+
+ public PackageHandler(IActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ this.logger = AdjustFactory.getLogger();
+
+ init(activityHandler, context, startPaused);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.context = context;
+ this.paused = startPaused;
+ }
+
+ // add a package to the queue
+ @Override
+ public void addPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.ADD;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ // try to send the oldest package
+ @Override
+ public void sendFirstPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_FIRST;
+ internalHandler.sendMessage(message);
+ }
+
+ // remove oldest package and try to send the next one
+ // (after success or possibly permanent failure)
+ @Override
+ public void sendNextPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_NEXT;
+ internalHandler.sendMessage(message);
+ }
+
+ // close the package to retry in the future (after temporary failure)
+ @Override
+ public void closeFirstPackage() {
+ isSending.set(false);
+ }
+
+ // interrupt the sending loop after the current request has finished
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ // allow sending requests again
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ // short info about how failing packages are handled
+ @Override
+ public String getFailureMessage() {
+ return "Will retry later.";
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ activityHandler.finishedTrackingActivity(jsonResponse);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ logger.debug("Sending click package (%s)", clickPackage);
+ logger.verbose("%s", clickPackage.getExtendedString());
+ requestHandler.sendClickPackage(clickPackage);
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 1;
+ private static final int ADD = 2;
+ private static final int SEND_NEXT = 3;
+ private static final int SEND_FIRST = 4;
+
+ private final WeakReference<PackageHandler> packageHandlerReference;
+
+ protected InternalHandler(Looper looper, PackageHandler packageHandler) {
+ super(looper);
+ this.packageHandlerReference = new WeakReference<PackageHandler>(packageHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ PackageHandler packageHandler = packageHandlerReference.get();
+ if (null == packageHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ packageHandler.initInternal();
+ break;
+ case ADD:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ packageHandler.addInternal(activityPackage);
+ break;
+ case SEND_FIRST:
+ packageHandler.sendFirstInternal();
+ break;
+ case SEND_NEXT:
+ packageHandler.sendNextInternal();
+ break;
+ }
+ }
+ }
+
+ // internal methods run in dedicated queue thread
+
+ private void initInternal() {
+ requestHandler = AdjustFactory.getRequestHandler(this);
+
+ isSending = new AtomicBoolean();
+
+ readPackageQueue();
+ }
+
+ private void addInternal(ActivityPackage newPackage) {
+ packageQueue.add(newPackage);
+ logger.debug("Added package %d (%s)", packageQueue.size(), newPackage);
+ logger.verbose("%s", newPackage.getExtendedString());
+
+ writePackageQueue();
+ }
+
+ private void sendFirstInternal() {
+ if (packageQueue.isEmpty()) {
+ return;
+ }
+
+ if (paused) {
+ logger.debug("Package handler is paused");
+ return;
+ }
+ if (isSending.getAndSet(true)) {
+ logger.verbose("Package handler is already sending");
+ return;
+ }
+
+ ActivityPackage firstPackage = packageQueue.get(0);
+ requestHandler.sendPackage(firstPackage);
+ }
+
+ private void sendNextInternal() {
+ packageQueue.remove(0);
+ writePackageQueue();
+ isSending.set(false);
+ sendFirstInternal();
+ }
+
+ private void readPackageQueue() {
+ try {
+ FileInputStream inputStream = context.openFileInput(PACKAGE_QUEUE_FILENAME);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ Object object = objectStream.readObject();
+ @SuppressWarnings("unchecked")
+ List<ActivityPackage> packageQueue = (List<ActivityPackage>) object;
+ logger.debug("Package handler read %d packages", packageQueue.size());
+ this.packageQueue = packageQueue;
+ return;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find package queue class");
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read package queue object");
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast package queue object");
+ } finally {
+ objectStream.close();
+ }
+ } catch (FileNotFoundException e) {
+ logger.verbose("Package queue file not found");
+ } catch (Exception e) {
+ logger.error("Failed to read package queue file");
+ }
+
+ // start with a fresh package queue in case of any exception
+ packageQueue = new ArrayList<ActivityPackage>();
+ }
+
+ public static Boolean deletePackageQueue(Context context) {
+ return context.deleteFile(PACKAGE_QUEUE_FILENAME);
+ }
+
+
+ private void writePackageQueue() {
+ try {
+ FileOutputStream outputStream = context.openFileOutput(PACKAGE_QUEUE_FILENAME, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(packageQueue);
+ logger.debug("Package handler wrote %d packages", packageQueue.size());
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize packages");
+ } finally {
+ objectStream.close();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to write packages (%s)", e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Reflection.java b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
new file mode 100644
index 000000000..d9d9a9dbc
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
@@ -0,0 +1,210 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import com.adjust.sdk.plugin.Plugin;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.PLUGINS;
+
+public class Reflection {
+
+ public static String getPlayAdId(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ String playAdid = (String) invokeInstanceMethod(AdvertisingInfoObject, "getId", null);
+
+ return playAdid;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ Boolean isLimitedTrackingEnabled = (Boolean) invokeInstanceMethod(AdvertisingInfoObject, "isLimitAdTrackingEnabled", null);
+
+ return !isLimitedTrackingEnabled;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static boolean isGooglePlayServicesAvailable(Context context) {
+ try {
+ Integer isGooglePlayServicesAvailableStatusCode = (Integer) invokeStaticMethod(
+ "com.google.android.gms.common.GooglePlayServicesUtil",
+ "isGooglePlayServicesAvailable",
+ new Class[]{Context.class}, context
+ );
+
+ boolean isGooglePlayServicesAvailable = (Boolean) isConnectionResultSuccess(isGooglePlayServicesAvailableStatusCode);
+
+ return isGooglePlayServicesAvailable;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static String getMacAddress(Context context) {
+ try {
+ String macSha1 = (String) invokeStaticMethod(
+ "com.adjust.sdk.plugin.MacAddressUtil",
+ "getMacAddress",
+ new Class[]{Context.class}, context
+ );
+
+ return macSha1;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getAndroidId(Context context) {
+ try {
+ String androidId = (String) invokeStaticMethod("com.adjust.sdk.plugin.AndroidIdUtil", "getAndroidId"
+ , new Class[]{Context.class}, context);
+
+ return androidId;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getSha1EmailAddress(Context context, String key) {
+ try {
+ String sha1EmailAddress = (String) invokeStaticMethod("com.adjust.sdk.plugin.EmailUtil", "getSha1EmailAddress"
+ , new Class[]{Context.class, String.class}, context, key);
+
+ return sha1EmailAddress;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ private static Object getAdvertisingInfoObject(Context context)
+ throws Exception {
+ return invokeStaticMethod("com.google.android.gms.ads.identifier.AdvertisingIdClient",
+ "getAdvertisingIdInfo",
+ new Class[]{Context.class}, context
+ );
+ }
+
+ private static boolean isConnectionResultSuccess(Integer statusCode) {
+ if (statusCode == null) {
+ return false;
+ }
+
+ try {
+ Class ConnectionResultClass = Class.forName("com.google.android.gms.common.ConnectionResult");
+
+ Field SuccessField = ConnectionResultClass.getField("SUCCESS");
+
+ int successStatusCode = SuccessField.getInt(null);
+
+ return successStatusCode == statusCode;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static Class forName(String className) {
+ try {
+ Class classObject = Class.forName(className);
+ return classObject;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createDefaultInstance(String className) {
+ Class classObject = forName(className);
+ Object instance = createDefaultInstance(classObject);
+ return instance;
+ }
+
+ public static Object createDefaultInstance(Class classObject) {
+ try {
+ Object instance = classObject.newInstance();
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createInstance(String className, Class[] cArgs, Object... args) {
+ try {
+ Class classObject = Class.forName(className);
+ @SuppressWarnings("unchecked")
+ Constructor constructor = classObject.getConstructor(cArgs);
+ Object instance = constructor.newInstance(args);
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object invokeStaticMethod(String className, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = Class.forName(className);
+
+ return invokeMethod(classObject, methodName, null, cArgs, args);
+ }
+
+ public static Object invokeInstanceMethod(Object instance, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = instance.getClass();
+
+ return invokeMethod(classObject, methodName, instance, cArgs, args);
+ }
+
+ public static Object invokeMethod(Class classObject, String methodName, Object instance, Class[] cArgs, Object... args)
+ throws Exception {
+ @SuppressWarnings("unchecked")
+ Method methodObject = classObject.getMethod(methodName, cArgs);
+
+ Object resultObject = methodObject.invoke(instance, args);
+
+ return resultObject;
+ }
+
+ public static Map<String, String> getPluginKeys(Context context) {
+ Map<String, String> pluginKeys = new HashMap<String, String>();
+
+ for (Plugin plugin : getPlugins()) {
+ Map.Entry<String, String> pluginEntry = plugin.getParameter(context);
+ if (pluginEntry != null) {
+ pluginKeys.put(pluginEntry.getKey(), pluginEntry.getValue());
+ }
+ }
+
+ if (pluginKeys.size() == 0) {
+ return null;
+ } else {
+ return pluginKeys;
+ }
+ }
+
+ private static List<Plugin> getPlugins() {
+ List<Plugin> plugins = new ArrayList<Plugin>(PLUGINS.size());
+
+ for (String pluginName : PLUGINS) {
+ Object pluginObject = Reflection.createDefaultInstance(pluginName);
+ if (pluginObject != null && pluginObject instanceof Plugin) {
+ plugins.add((Plugin) pluginObject);
+ }
+ }
+
+ return plugins;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
new file mode 100644
index 000000000..84d45d0ce
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
@@ -0,0 +1,210 @@
+//
+// RequestHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.utils.URLEncodedUtils;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class RequestHandler extends HandlerThread implements IRequestHandler {
+ private InternalHandler internalHandler;
+ private IPackageHandler packageHandler;
+ private HttpClient httpClient;
+ private ILogger logger;
+
+ public RequestHandler(IPackageHandler packageHandler) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ this.logger = AdjustFactory.getLogger();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ init(packageHandler);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IPackageHandler packageHandler) {
+ this.packageHandler = packageHandler;
+ }
+
+ @Override
+ public void sendPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_CLICK;
+ message.obj = clickPackage;
+ internalHandler.sendMessage(message);
+
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 72401;
+ private static final int SEND = 72400;
+ private static final int SEND_CLICK = 72402;
+
+ private final WeakReference<RequestHandler> requestHandlerReference;
+
+ protected InternalHandler(Looper looper, RequestHandler requestHandler) {
+ super(looper);
+ this.requestHandlerReference = new WeakReference<RequestHandler>(requestHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ RequestHandler requestHandler = requestHandlerReference.get();
+ if (null == requestHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ requestHandler.initInternal();
+ break;
+ case SEND:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(activityPackage, true);
+ break;
+ case SEND_CLICK:
+ ActivityPackage clickPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(clickPackage, false);
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ httpClient = Util.getHttpClient();
+ }
+
+ private void sendInternal(ActivityPackage activityPackage, boolean sendToPackageHandler) {
+ try {
+ HttpUriRequest request = getRequest(activityPackage);
+ HttpResponse response = httpClient.execute(request);
+ requestFinished(response, sendToPackageHandler);
+ } catch (UnsupportedEncodingException e) {
+ sendNextPackage(activityPackage, "Failed to encode parameters", e, sendToPackageHandler);
+ } catch (ClientProtocolException e) {
+ closePackage(activityPackage, "Client protocol error", e, sendToPackageHandler);
+ } catch (SocketTimeoutException e) {
+ closePackage(activityPackage, "Request timed out", e, sendToPackageHandler);
+ } catch (IOException e) {
+ closePackage(activityPackage, "Request failed", e, sendToPackageHandler);
+ } catch (Throwable e) {
+ sendNextPackage(activityPackage, "Runtime exception", e, sendToPackageHandler);
+ }
+ }
+
+ private void requestFinished(HttpResponse response, boolean sendToPackageHandler) {
+ JSONObject jsonResponse = Util.parseJsonResponse(response, logger);
+
+ if (jsonResponse == null) {
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ return;
+ }
+
+ packageHandler.finishedTrackingActivity(jsonResponse);
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ // close current package because it failed
+ private void closePackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String packageMessage = activityPackage.getFailureMessage();
+ final String handlerMessage = packageHandler.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s) %s", packageMessage, reasonString, handlerMessage);
+
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ }
+
+ // send next package because the current package failed
+ private void sendNextPackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String failureMessage = activityPackage.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s)", failureMessage, reasonString);
+
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ private String getReasonString(String message, Throwable throwable) {
+ if (throwable != null) {
+ return String.format("%s: %s", message, throwable);
+ } else {
+ return String.format("%s", message);
+ }
+ }
+
+ private HttpUriRequest getRequest(ActivityPackage activityPackage) throws UnsupportedEncodingException {
+ String url = Constants.BASE_URL + activityPackage.getPath();
+ HttpPost request = new HttpPost(url);
+
+ String language = Locale.getDefault().getLanguage();
+ request.addHeader("Client-SDK", activityPackage.getClientSdk());
+ request.addHeader("Accept-Language", language);
+
+ List<NameValuePair> pairs = new ArrayList<NameValuePair>();
+ for (Map.Entry<String, String> entry : activityPackage.getParameters().entrySet()) {
+ NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
+ pairs.add(pair);
+ }
+
+ long now = System.currentTimeMillis();
+ String dateString = Util.dateFormat(now);
+ NameValuePair sentAtPair = new BasicNameValuePair("sent_at", dateString);
+ pairs.add(sentAtPair);
+
+ UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs);
+ entity.setContentType(URLEncodedUtils.CONTENT_TYPE);
+ request.setEntity(entity);
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
new file mode 100644
index 000000000..799fb8982
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
@@ -0,0 +1,38 @@
+package com.adjust.sdk;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class UnitTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //setContentView(com.adjust.sdk.test.R.layout.activity_unit_test);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(com.adjust.sdk.test.R.menu.menu_unit_test, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+/* int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == com.adjust.sdk.test.R.id.action_settings) {
+ return true;
+ }
+*/
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Util.java b/mobile/android/thirdparty/com/adjust/sdk/Util.java
new file mode 100644
index 000000000..84c47f87e
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Util.java
@@ -0,0 +1,202 @@
+//
+// Util.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Collects utility functions used by Adjust.
+ */
+public class Util {
+
+ private static SimpleDateFormat dateFormat;
+ private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z";
+
+ protected static String createUuid() {
+ return UUID.randomUUID().toString();
+ }
+
+ public static String quote(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("\\s");
+ Matcher matcher = pattern.matcher(string);
+ if (!matcher.find()) {
+ return string;
+ }
+
+ return String.format("'%s'", string);
+ }
+
+ public static String dateFormat(long date) {
+ if (null == dateFormat) {
+ dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+ }
+ return dateFormat.format(date);
+ }
+
+ public static String getPlayAdId(Context context) {
+ return Reflection.getPlayAdId(context);
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ return Reflection.isPlayTrackingEnabled(context);
+ }
+
+ public static <T> T readObject(Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileInputStream inputStream = context.openFileInput(filename);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ @SuppressWarnings("unchecked")
+ T t = (T) objectStream.readObject();
+ logger.debug("Read %s: %s", objectName, t);
+ return t;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find %s class", objectName);
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read %s object", objectName);
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast %s object", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (FileNotFoundException e) {
+ logger.verbose("%s file not found", objectName);
+ } catch (Exception e) {
+ logger.error("Failed to open %s file for reading (%s)", objectName, e);
+ }
+
+ return null;
+ }
+
+ public static <T> void writeObject(T object, Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileOutputStream outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(object);
+ logger.debug("Wrote %s: %s", objectName, object);
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize %s", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to open %s for writing (%s)", objectName, e);
+ }
+ }
+
+ public static String parseResponse(HttpResponse httpResponse, ILogger logger) {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ String response = out.toString().trim();
+ logger.verbose("Response: %s", response);
+ return response;
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e);
+ return null;
+ }
+ }
+
+ public static JSONObject parseJsonResponse(HttpResponse httpResponse, ILogger logger) {
+ if (httpResponse == null) {
+ return null;
+ }
+ String stringResponse = null;
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ stringResponse = out.toString().trim();
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e.getMessage());
+ }
+
+ logger.verbose("Response: %s", stringResponse);
+ if (stringResponse == null) return null;
+
+ JSONObject jsonResponse = null;
+ try {
+ jsonResponse = new JSONObject(stringResponse);
+ } catch (JSONException e) {
+ logger.error("Failed to parse json response: %s (%s)", stringResponse, e.getMessage());
+ }
+
+ if (jsonResponse == null) return null;
+
+ String message = jsonResponse.optString("message", null);
+
+ if (message == null) {
+ message = "No message found";
+ }
+
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ logger.info("%s", message);
+ } else {
+ logger.error("%s", message);
+ }
+
+ return jsonResponse;
+ }
+
+ public static HttpClient getHttpClient() {
+ HttpParams httpParams = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(httpParams, Constants.CONNECTION_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(httpParams, Constants.SOCKET_TIMEOUT);
+ return AdjustFactory.getHttpClient(httpParams);
+ }
+
+ public static boolean checkPermission(Context context, String permission) {
+ int result = context.checkCallingOrSelfPermission(permission);
+ return result == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
new file mode 100644
index 000000000..96a072287
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
@@ -0,0 +1,10 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+
+public class AndroidIdUtil {
+ public static String getAndroidId(final Context context) {
+ return Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
new file mode 100644
index 000000000..c8bdbadd7
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
@@ -0,0 +1,82 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Locale;
+
+public class MacAddressUtil {
+ public static String getMacAddress(Context context) {
+ final String rawAddress = getRawMacAddress(context);
+ if (rawAddress == null) {
+ return null;
+ }
+ final String upperAddress = rawAddress.toUpperCase(Locale.US);
+ return removeSpaceString(upperAddress);
+ }
+
+ private static String getRawMacAddress(Context context) {
+ // android devices should have a wlan address
+ final String wlanAddress = loadAddress("wlan0");
+ if (wlanAddress != null) {
+ return wlanAddress;
+ }
+
+ // emulators should have an ethernet address
+ final String ethAddress = loadAddress("eth0");
+ if (ethAddress != null) {
+ return ethAddress;
+ }
+
+ // query the wifi manager (requires the ACCESS_WIFI_STATE permission)
+ try {
+ final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ final String wifiAddress = wifiManager.getConnectionInfo().getMacAddress();
+ if (wifiAddress != null) {
+ return wifiAddress;
+ }
+ } catch (Exception e) {
+ /* no-op */
+ }
+
+ return null;
+ }
+
+ private static String loadAddress(final String interfaceName) {
+ try {
+ final String filePath = "/sys/class/net/" + interfaceName + "/address";
+ final StringBuilder fileData = new StringBuilder(1000);
+ final BufferedReader reader = new BufferedReader(new FileReader(filePath), 1024);
+ final char[] buf = new char[1024];
+ int numRead;
+
+ String readData;
+ while ((numRead = reader.read(buf)) != -1) {
+ readData = String.valueOf(buf, 0, numRead);
+ fileData.append(readData);
+ }
+
+ reader.close();
+ return fileData.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static String removeSpaceString(final String inputString) {
+ if (inputString == null) {
+ return null;
+ }
+
+ String outputString = inputString.replaceAll("\\s", "");
+ if (TextUtils.isEmpty(outputString)) {
+ return null;
+ }
+
+ return outputString;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
new file mode 100644
index 000000000..ab704e6d3
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
@@ -0,0 +1,12 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+
+import java.util.Map;
+
+/**
+ * Created by pfms on 18/09/14.
+ */
+public interface Plugin {
+ Map.Entry<String, String> getParameter(Context context);
+}