diff --git a/Adjust/.gitignore b/Adjust/.gitignore new file mode 100644 index 000000000..afbdab33e --- /dev/null +++ b/Adjust/.gitignore @@ -0,0 +1,6 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build diff --git a/Adjust/AndroidManifest.xml b/Adjust/AndroidManifest.xml deleted file mode 100644 index 9c1ed24f4..000000000 --- a/Adjust/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Adjust/adjust/.gitignore b/Adjust/adjust/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/Adjust/adjust/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Adjust/adjust/build.gradle b/Adjust/adjust/build.gradle new file mode 100644 index 000000000..92255c2ab --- /dev/null +++ b/Adjust/adjust/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 21 + versionCode 1 + versionName "4.0.0" + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} + +task jar(type: Jar) { + from 'src/main/java' +} \ No newline at end of file diff --git a/Adjust/adjust/src/main/AndroidManifest.xml b/Adjust/adjust/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d3e803eec --- /dev/null +++ b/Adjust/adjust/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityHandler.java new file mode 100644 index 000000000..236c9ea4e --- /dev/null +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityHandler.java @@ -0,0 +1,759 @@ +// +// 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 sessionHandlerReference; + + protected SessionHandler(Looper looper, ActivityHandler sessionHandler) { + super(looper); + this.sessionHandlerReference = new WeakReference(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 queryStringParameters = new HashMap(); + 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 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 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() { + activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME); + } + + private void readAttribution() { + attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME); + } + + 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/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityKind.java b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityKind.java new file mode 100644 index 000000000..a255b83a9 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/src/com/adjust/sdk/ActivityPackage.java b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityPackage.java similarity index 80% rename from Adjust/src/com/adjust/sdk/ActivityPackage.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/ActivityPackage.java index 04a442194..27ab969fd 100644 --- a/Adjust/src/com/adjust/sdk/ActivityPackage.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityPackage.java @@ -16,14 +16,13 @@ public class ActivityPackage implements Serializable { private static final long serialVersionUID = -35935556512024097L; // data - private String path; - private String userAgent; - private String clientSdk; + private String path; + private String clientSdk; private Map parameters; // logs private ActivityKind activityKind; - private String suffix; + private String suffix; public String getPath() { return path; @@ -33,14 +32,6 @@ public void setPath(String path) { this.path = path; } - public String getUserAgent() { - return userAgent; - } - - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; - } - public String getClientSdk() { return clientSdk; } @@ -61,7 +52,7 @@ public ActivityKind getActivityKind() { return activityKind; } - public void setActivityKind(ActivityKind activityKind ) { + public void setActivityKind(ActivityKind activityKind) { this.activityKind = activityKind; } @@ -80,13 +71,12 @@ public String toString() { public String getExtendedString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("Path: %s\n", path)); - builder.append(String.format("UserAgent: %s\n", userAgent)); builder.append(String.format("ClientSdk: %s\n", clientSdk)); if (parameters != null) { builder.append("Parameters:"); - for (Map.Entry entity : parameters.entrySet()) { - builder.append(String.format("\n\t%-16s %s", entity.getKey(), entity.getValue())); + for (Map.Entry entry : parameters.entrySet()) { + builder.append(String.format("\n\t%-16s %s", entry.getKey(), entry.getValue())); } } return builder.toString(); diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityState.java b/Adjust/adjust/src/main/java/com/adjust/sdk/ActivityState.java new file mode 100644 index 000000000..41ad2ca3b --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/Adjust.java b/Adjust/adjust/src/main/java/com/adjust/sdk/Adjust.java new file mode 100644 index 000000000..3b81a077b --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustAttribution.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustAttribution.java new file mode 100644 index 000000000..4e3abb017 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustConfig.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustConfig.java new file mode 100644 index 000000000..148a5f670 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustEvent.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustEvent.java new file mode 100644 index 000000000..f03718183 --- /dev/null +++ b/Adjust/adjust/src/main/java/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 callbackParameters; + Map 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 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 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/Adjust/src/com/adjust/sdk/AdjustFactory.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustFactory.java similarity index 57% rename from Adjust/src/com/adjust/sdk/AdjustFactory.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/AdjustFactory.java index d54ac1a43..1ad772b24 100644 --- a/Adjust/src/com/adjust/sdk/AdjustFactory.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustFactory.java @@ -1,25 +1,31 @@ package com.adjust.sdk; +import android.content.Context; + import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.HttpParams; -import android.content.Context; - public class AdjustFactory { private static IPackageHandler packageHandler = null; private static IRequestHandler requestHandler = null; - private static Logger logger = 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 dropOfflineActivities) { + public static IPackageHandler getPackageHandler(ActivityHandler activityHandler, + Context context, + boolean startPaused) { if (packageHandler == null) { - return new PackageHandler(activityHandler, context, dropOfflineActivities); + return new PackageHandler(activityHandler, context, startPaused); } + packageHandler.init(activityHandler, context, startPaused); return packageHandler; } @@ -27,13 +33,14 @@ public static IRequestHandler getRequestHandler(IPackageHandler packageHandler) if (requestHandler == null) { return new RequestHandler(packageHandler); } + requestHandler.init(packageHandler); return requestHandler; } - public static Logger getLogger() { + public static ILogger getLogger() { if (logger == null) { // Logger needs to be "static" to retain the configuration throughout the app - logger = new LogCatLogger(); + logger = new Logger(); } return logger; } @@ -52,6 +59,13 @@ public static long getTimerInterval() { return timerInterval; } + public static long getTimerStart() { + if (timerStart == -1) { + return 0; + } + return timerStart; + } + public static long getSessionInterval() { if (sessionInterval == -1) { return Constants.THIRTY_MINUTES; @@ -66,6 +80,24 @@ public static long getSubsessionInterval() { 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; } @@ -74,7 +106,7 @@ public static void setRequestHandler(IRequestHandler requestHandler) { AdjustFactory.requestHandler = requestHandler; } - public static void setLogger(Logger logger) { + public static void setLogger(ILogger logger) { AdjustFactory.logger = logger; } @@ -86,6 +118,10 @@ 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; } @@ -94,4 +130,12 @@ 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/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustInstance.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustInstance.java new file mode 100644 index 000000000..158fb7ca1 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/src/com/adjust/sdk/ReferrerReceiver.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustReferrerReceiver.java similarity index 70% rename from Adjust/src/com/adjust/sdk/ReferrerReceiver.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/AdjustReferrerReceiver.java index eab1536fb..cfeecd8d0 100644 --- a/Adjust/src/com/adjust/sdk/ReferrerReceiver.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/AdjustReferrerReceiver.java @@ -3,22 +3,18 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import static com.adjust.sdk.Constants.ENCODING; -import static com.adjust.sdk.Constants.MALFORMED; -import static com.adjust.sdk.Constants.REFERRER; 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 ReferrerReceiver extends BroadcastReceiver { - - protected static final String REFERRER_KEY = "AdjustInstallReferrer"; - +public class AdjustReferrerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String rawReferrer = intent.getStringExtra(REFERRER); @@ -33,7 +29,7 @@ public void onReceive(Context context, Intent intent) { referrer = MALFORMED; } - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - preferences.edit().putString(REFERRER_KEY, referrer).commit(); + AdjustInstance adjust = Adjust.getDefaultInstance(); + adjust.sendReferrer(referrer); } } diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/AttributionHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/AttributionHandler.java new file mode 100644 index 000000000..2a2bef03d --- /dev/null +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/AttributionHandler.java @@ -0,0 +1,155 @@ +package com.adjust.sdk; + +import android.net.Uri; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.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 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/Adjust/adjust/src/main/java/com/adjust/sdk/Constants.java b/Adjust/adjust/src/main/java/com/adjust/sdk/Constants.java new file mode 100644 index 000000000..7a97cb2f4 --- /dev/null +++ b/Adjust/adjust/src/main/java/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 PLUGINS = Arrays.asList(); +} diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/DeviceInfo.java b/Adjust/adjust/src/main/java/com/adjust/sdk/DeviceInfo.java new file mode 100644 index 000000000..5cccb77f4 --- /dev/null +++ b/Adjust/adjust/src/main/java/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 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/Adjust/adjust/src/main/java/com/adjust/sdk/IActivityHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/IActivityHandler.java new file mode 100644 index 000000000..10b92205d --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/IAttributionHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/IAttributionHandler.java new file mode 100644 index 000000000..d4e701f75 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/ILogger.java b/Adjust/adjust/src/main/java/com/adjust/sdk/ILogger.java new file mode 100644 index 000000000..28f92af4b --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/src/com/adjust/sdk/IPackageHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/IPackageHandler.java similarity index 58% rename from Adjust/src/com/adjust/sdk/IPackageHandler.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/IPackageHandler.java index 0334aaa50..99c300364 100644 --- a/Adjust/src/com/adjust/sdk/IPackageHandler.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/IPackageHandler.java @@ -1,8 +1,12 @@ 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(); @@ -17,7 +21,7 @@ public interface IPackageHandler { public String getFailureMessage(); - public boolean dropsOfflineActivities(); + public void finishedTrackingActivity(JSONObject jsonResponse); - public void finishedTrackingActivity(ActivityPackage activityPackage, ResponseData responseData, JSONObject jsonResponse); + public void sendClickPackage(ActivityPackage clickPackage); } diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/IRequestHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/IRequestHandler.java new file mode 100644 index 000000000..5b18e2ee9 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/LogLevel.java b/Adjust/adjust/src/main/java/com/adjust/sdk/LogLevel.java new file mode 100644 index 000000000..5c0b410c2 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/Logger.java b/Adjust/adjust/src/main/java/com/adjust/sdk/Logger.java new file mode 100644 index 000000000..86a644d4a --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/OnAttributionChangedListener.java b/Adjust/adjust/src/main/java/com/adjust/sdk/OnAttributionChangedListener.java new file mode 100644 index 000000000..137d50d4d --- /dev/null +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/OnAttributionChangedListener.java @@ -0,0 +1,5 @@ +package com.adjust.sdk; + +public interface OnAttributionChangedListener { + public void onAttributionChanged(AdjustAttribution attribution); +} diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/PackageBuilder.java b/Adjust/adjust/src/main/java/com/adjust/sdk/PackageBuilder.java new file mode 100644 index 000000000..3a43045fd --- /dev/null +++ b/Adjust/adjust/src/main/java/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 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 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 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 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 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 getDefaultParameters() { + Map parameters = new HashMap(); + + injectDeviceInfo(parameters); + injectConfig(parameters); + injectActivityState(parameters); + addDate(parameters, "created_at", createdAt); + + // general + checkDeviceIds(parameters); + + return parameters; + } + + private Map getIdsParameters() { + Map parameters = new HashMap(); + + injectDeviceInfoIds(parameters); + injectConfig(parameters); + injectActivityStateIds(parameters); + + checkDeviceIds(parameters); + + return parameters; + } + + private void injectDeviceInfo(Map 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 parameters) { + addString(parameters, "mac_sha1", deviceInfo.macSha1); + addString(parameters, "mac_md5", deviceInfo.macShortMd5); + addString(parameters, "android_id", deviceInfo.androidId); + } + + private void injectConfig(Map 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 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 parameters) { + addString(parameters, "android_uuid", activityState.uuid); + } + + private void injectAttribution(Map 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 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 parameters) { + if (deviceInfo.pluginKeys == null) { + return; + } + + for (Map.Entry 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 parameters, String key, String value) { + if (TextUtils.isEmpty(value)) { + return; + } + + parameters.put(key, value); + } + + private void addInt(Map parameters, String key, long value) { + if (value < 0) { + return; + } + + String valueString = Long.toString(value); + addString(parameters, key, valueString); + } + + private void addDate(Map parameters, String key, long value) { + if (value < 0) { + return; + } + + String dateString = Util.dateFormat(value); + addString(parameters, key, dateString); + } + + private void addDuration(Map parameters, String key, long durationInMilliSeconds) { + if (durationInMilliSeconds < 0) { + return; + } + + long durationInSeconds = (durationInMilliSeconds + 500) / 1000; + addInt(parameters, key, durationInSeconds); + } + + private void addMapJson(Map parameters, String key, Map 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 parameters, String key, Boolean value) { + if (value == null) { + return; + } + + int intValue = value ? 1 : 0; + + addInt(parameters, key, intValue); + } + + private void addDouble(Map parameters, String key, Double value) { + if (value == null) return; + + String doubleString = String.format("%.5f", value); + + addString(parameters, key, doubleString); + } +} diff --git a/Adjust/src/com/adjust/sdk/PackageHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/PackageHandler.java similarity index 79% rename from Adjust/src/com/adjust/sdk/PackageHandler.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/PackageHandler.java index 2feb5a7f6..d0a84ccd1 100644 --- a/Adjust/src/com/adjust/sdk/PackageHandler.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/PackageHandler.java @@ -9,6 +9,14 @@ 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; @@ -24,44 +32,42 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import org.json.JSONObject; - -import android.content.Context; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; - // persistent public class PackageHandler extends HandlerThread implements IPackageHandler { private static final String PACKAGE_QUEUE_FILENAME = "AdjustIoPackageQueue"; - private final InternalHandler internalHandler; - private IRequestHandler requestHandler; - private ActivityHandler activityHandler; - private List packageQueue; - private AtomicBoolean isSending; - private boolean paused; - private Context context; - private boolean dropOfflineActivities; - private Logger logger; - - public PackageHandler(ActivityHandler activityHandler, Context context, boolean dropOfflineActivities) { + private final InternalHandler internalHandler; + private IRequestHandler requestHandler; + private IActivityHandler activityHandler; + private List 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(); - this.activityHandler = activityHandler; - this.context = context; - this.dropOfflineActivities = dropOfflineActivities; + 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) { @@ -91,11 +97,7 @@ public void sendNextPackage() { // close the package to retry in the future (after temporary failure) @Override public void closeFirstPackage() { - if (dropOfflineActivities) { - sendNextPackage(); - } else { - isSending.set(false); - } + isSending.set(false); } // interrupt the sending loop after the current request has finished @@ -113,35 +115,25 @@ public void resumeSending() { // short info about how failing packages are handled @Override public String getFailureMessage() { - if (dropOfflineActivities) { - return "Dropping offline activity."; - } else { - return "Will retry later."; - } + return "Will retry later."; } @Override - public boolean dropsOfflineActivities() { - return dropOfflineActivities; + public void finishedTrackingActivity(JSONObject jsonResponse) { + activityHandler.finishedTrackingActivity(jsonResponse); } @Override - public void finishedTrackingActivity(ActivityPackage activityPackage, ResponseData responseData, JSONObject jsonResponse) { - responseData.setActivityKind(activityPackage.getActivityKind()); - - String deepLink = null; - - if (jsonResponse != null) { - deepLink = jsonResponse.optString("deeplink", null); - } - - activityHandler.finishedTrackingActivity(responseData, deepLink); + 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 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 packageHandlerReference; @@ -191,7 +183,7 @@ private void initInternal() { private void addInternal(ActivityPackage newPackage) { packageQueue.add(newPackage); logger.debug("Added package %d (%s)", packageQueue.size(), newPackage); - logger.verbose(newPackage.getExtendedString()); + logger.verbose("%s", newPackage.getExtendedString()); writePackageQueue(); } @@ -222,11 +214,6 @@ private void sendNextInternal() { } private void readPackageQueue() { - if (dropOfflineActivities) { - packageQueue = new ArrayList(); - return; // don't read old packages when offline tracking is disabled - } - try { FileInputStream inputStream = context.openFileInput(PACKAGE_QUEUE_FILENAME); BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); @@ -266,10 +253,6 @@ public static Boolean deletePackageQueue(Context context) { private void writePackageQueue() { - if (dropOfflineActivities) { - return; // don't write packages when offline tracking is disabled - } - try { FileOutputStream outputStream = context.openFileOutput(PACKAGE_QUEUE_FILENAME, Context.MODE_PRIVATE); BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream); diff --git a/Adjust/src/com/adjust/sdk/Reflection.java b/Adjust/adjust/src/main/java/com/adjust/sdk/Reflection.java similarity index 69% rename from Adjust/src/com/adjust/sdk/Reflection.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/Reflection.java index 4e4faf0a9..d9d9a9dbc 100644 --- a/Adjust/src/com/adjust/sdk/Reflection.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/Reflection.java @@ -1,10 +1,18 @@ 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 android.content.Context; +import static com.adjust.sdk.Constants.PLUGINS; public class Reflection { @@ -15,8 +23,7 @@ public static String getPlayAdId(Context context) { String playAdid = (String) invokeInstanceMethod(AdvertisingInfoObject, "getId", null); return playAdid; - } - catch (Throwable t) { + } catch (Throwable t) { return null; } } @@ -28,8 +35,7 @@ public static Boolean isPlayTrackingEnabled(Context context) { Boolean isLimitedTrackingEnabled = (Boolean) invokeInstanceMethod(AdvertisingInfoObject, "isLimitAdTrackingEnabled", null); return !isLimitedTrackingEnabled; - } - catch (Throwable t) { + } catch (Throwable t) { return null; } } @@ -39,14 +45,13 @@ public static boolean isGooglePlayServicesAvailable(Context context) { Integer isGooglePlayServicesAvailableStatusCode = (Integer) invokeStaticMethod( "com.google.android.gms.common.GooglePlayServicesUtil", "isGooglePlayServicesAvailable", - new Class[] {Context.class}, context + new Class[]{Context.class}, context ); boolean isGooglePlayServicesAvailable = (Boolean) isConnectionResultSuccess(isGooglePlayServicesAvailableStatusCode); return isGooglePlayServicesAvailable; - } - catch (Throwable t) { + } catch (Throwable t) { return false; } } @@ -56,12 +61,11 @@ public static String getMacAddress(Context context) { String macSha1 = (String) invokeStaticMethod( "com.adjust.sdk.plugin.MacAddressUtil", "getMacAddress", - new Class[] {Context.class}, context + new Class[]{Context.class}, context ); return macSha1; - } - catch (Throwable t) { + } catch (Throwable t) { return null; } } @@ -69,11 +73,21 @@ public static String getMacAddress(Context context) { public static String getAndroidId(Context context) { try { String androidId = (String) invokeStaticMethod("com.adjust.sdk.plugin.AndroidIdUtil", "getAndroidId" - ,new Class[] {Context.class}, context); + , new Class[]{Context.class}, context); return androidId; + } catch (Throwable t) { + return null; } - catch (Throwable t) { + } + + 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; } } @@ -82,7 +96,7 @@ private static Object getAdvertisingInfoObject(Context context) throws Exception { return invokeStaticMethod("com.google.android.gms.ads.identifier.AdvertisingIdClient", "getAdvertisingIdInfo", - new Class[] {Context.class} , context + new Class[]{Context.class}, context ); } @@ -99,8 +113,7 @@ private static boolean isConnectionResultSuccess(Integer statusCode) { int successStatusCode = SuccessField.getInt(null); return successStatusCode == statusCode; - } - catch (Throwable t) { + } catch (Throwable t) { return false; } } @@ -124,7 +137,7 @@ public static Object createDefaultInstance(Class classObject) { try { Object instance = classObject.newInstance(); return instance; - }catch (Throwable t) { + } catch (Throwable t) { return null; } } @@ -132,10 +145,11 @@ public static Object createDefaultInstance(Class classObject) { 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) { + } catch (Throwable t) { return null; } } @@ -156,10 +170,41 @@ public static Object invokeInstanceMethod(Object instance, String methodName, Cl 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 getPluginKeys(Context context) { + Map pluginKeys = new HashMap(); + + for (Plugin plugin : getPlugins()) { + Map.Entry 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 getPlugins() { + List plugins = new ArrayList(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/Adjust/src/com/adjust/sdk/RequestHandler.java b/Adjust/adjust/src/main/java/com/adjust/sdk/RequestHandler.java similarity index 64% rename from Adjust/src/com/adjust/sdk/RequestHandler.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/RequestHandler.java index 412009b14..49a46c8de 100644 --- a/Adjust/src/com/adjust/sdk/RequestHandler.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/RequestHandler.java @@ -9,18 +9,12 @@ package com.adjust.sdk; -import java.io.ByteArrayOutputStream; -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; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; @@ -29,24 +23,22 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; import org.json.JSONObject; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; +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 static final int CONNECTION_TIMEOUT = Constants.ONE_MINUTE; - private static final int SOCKET_TIMEOUT = Constants.ONE_MINUTE; - private InternalHandler internalHandler; private IPackageHandler packageHandler; - private HttpClient httpClient; - private Logger logger; + private HttpClient httpClient; + private ILogger logger; public RequestHandler(IPackageHandler packageHandler) { super(Constants.LOGTAG, MIN_PRIORITY); @@ -55,13 +47,18 @@ public RequestHandler(IPackageHandler packageHandler) { this.logger = AdjustFactory.getLogger(); this.internalHandler = new InternalHandler(getLooper(), this); - this.packageHandler = packageHandler; + 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(); @@ -70,9 +67,19 @@ public void sendPackage(ActivityPackage 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 requestHandlerReference; @@ -96,90 +103,75 @@ public void handleMessage(Message message) { break; case SEND: ActivityPackage activityPackage = (ActivityPackage) message.obj; - requestHandler.sendInternal(activityPackage); + requestHandler.sendInternal(activityPackage, true); + break; + case SEND_CLICK: + ActivityPackage clickPackage = (ActivityPackage) message.obj; + requestHandler.sendInternal(clickPackage, false); break; } } } private void initInternal() { - HttpParams httpParams = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParams, CONNECTION_TIMEOUT); - HttpConnectionParams.setSoTimeout(httpParams, SOCKET_TIMEOUT); - httpClient = AdjustFactory.getHttpClient(httpParams); + httpClient = Util.getHttpClient(); } - private void sendInternal(ActivityPackage activityPackage) { + private void sendInternal(ActivityPackage activityPackage, boolean sendToPackageHandler) { try { HttpUriRequest request = getRequest(activityPackage); HttpResponse response = httpClient.execute(request); - requestFinished(response, activityPackage); + requestFinished(response, sendToPackageHandler); } catch (UnsupportedEncodingException e) { - sendNextPackage(activityPackage, "Failed to encode parameters", e); + sendNextPackage(activityPackage, "Failed to encode parameters", e, sendToPackageHandler); } catch (ClientProtocolException e) { - closePackage(activityPackage, "Client protocol error", e); + closePackage(activityPackage, "Client protocol error", e, sendToPackageHandler); } catch (SocketTimeoutException e) { - closePackage(activityPackage, "Request timed out", e); + closePackage(activityPackage, "Request timed out", e, sendToPackageHandler); } catch (IOException e) { - closePackage(activityPackage, "Request failed", e); + closePackage(activityPackage, "Request failed", e, sendToPackageHandler); } catch (Throwable e) { - sendNextPackage(activityPackage, "Runtime exception", e); + sendNextPackage(activityPackage, "Runtime exception", e, sendToPackageHandler); } } - private void requestFinished(HttpResponse response, ActivityPackage activityPackage) { - int statusCode = response.getStatusLine().getStatusCode(); - String responseString = parseResponse(response); - JSONObject jsonResponse = Util.buildJsonObject(responseString); - ResponseData responseData = ResponseData.fromJson(jsonResponse, responseString); + private void requestFinished(HttpResponse response, boolean sendToPackageHandler) { + JSONObject jsonResponse = Util.parseJsonResponse(response, logger); - if (HttpStatus.SC_OK == statusCode) { - // success - responseData.setWasSuccess(true); - logger.info(activityPackage.getSuccessMessage()); - } else { - // wrong status code - logger.error("%s. (%s)", activityPackage.getFailureMessage(), responseData.getError()); + if (jsonResponse == null) { + if (sendToPackageHandler) { + packageHandler.closeFirstPackage(); + } + return; } - packageHandler.finishedTrackingActivity(activityPackage, responseData, jsonResponse); - packageHandler.sendNextPackage(); - } - - private String parseResponse(HttpResponse response) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - response.getEntity().writeTo(out); - out.close(); - return out.toString().trim(); - } catch (Exception e) { - logger.error("Failed to parse response (%s)", e); - return "Failed to parse response"; + packageHandler.finishedTrackingActivity(jsonResponse); + if (sendToPackageHandler) { + packageHandler.sendNextPackage(); } } // close current package because it failed - private void closePackage(ActivityPackage activityPackage, String message, Throwable throwable) { + 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); - ResponseData responseData = ResponseData.fromError(reasonString); - responseData.setWillRetry(!packageHandler.dropsOfflineActivities()); - packageHandler.finishedTrackingActivity(activityPackage, responseData, null); - packageHandler.closeFirstPackage(); + if (sendToPackageHandler) { + packageHandler.closeFirstPackage(); + } } // send next package because the current package failed - private void sendNextPackage(ActivityPackage activityPackage, String message, Throwable throwable) { + 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); - ResponseData responseData = ResponseData.fromError(reasonString); - packageHandler.finishedTrackingActivity(activityPackage, responseData, null); - packageHandler.sendNextPackage(); + if (sendToPackageHandler) { + packageHandler.sendNextPackage(); + } } private String getReasonString(String message, Throwable throwable) { @@ -195,13 +187,12 @@ private HttpUriRequest getRequest(ActivityPackage activityPackage) throws Unsupp HttpPost request = new HttpPost(url); String language = Locale.getDefault().getLanguage(); - request.addHeader("User-Agent", activityPackage.getUserAgent()); request.addHeader("Client-SDK", activityPackage.getClientSdk()); request.addHeader("Accept-Language", language); List pairs = new ArrayList(); - for (Map.Entry entity : activityPackage.getParameters().entrySet()) { - NameValuePair pair = new BasicNameValuePair(entity.getKey(), entity.getValue()); + for (Map.Entry entry : activityPackage.getParameters().entrySet()) { + NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue()); pairs.add(pair); } diff --git a/Adjust/adjust/src/main/java/com/adjust/sdk/UnitTestActivity.java b/Adjust/adjust/src/main/java/com/adjust/sdk/UnitTestActivity.java new file mode 100644 index 000000000..799fb8982 --- /dev/null +++ b/Adjust/adjust/src/main/java/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/Adjust/adjust/src/main/java/com/adjust/sdk/Util.java b/Adjust/adjust/src/main/java/com/adjust/sdk/Util.java new file mode 100644 index 000000000..a893d09f9 --- /dev/null +++ b/Adjust/adjust/src/main/java/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 org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.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 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 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/Adjust/src/com/adjust/sdk/plugin/AndroidIdUtil.java b/Adjust/adjust/src/main/java/com/adjust/sdk/plugin/AndroidIdUtil.java similarity index 100% rename from Adjust/src/com/adjust/sdk/plugin/AndroidIdUtil.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/plugin/AndroidIdUtil.java diff --git a/Adjust/src/com/adjust/sdk/plugin/MacAddressUtil.java b/Adjust/adjust/src/main/java/com/adjust/sdk/plugin/MacAddressUtil.java similarity index 100% rename from Adjust/src/com/adjust/sdk/plugin/MacAddressUtil.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/plugin/MacAddressUtil.java index 13b24901a..c8bdbadd7 100644 --- a/Adjust/src/com/adjust/sdk/plugin/MacAddressUtil.java +++ b/Adjust/adjust/src/main/java/com/adjust/sdk/plugin/MacAddressUtil.java @@ -1,14 +1,14 @@ 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; -import android.content.Context; -import android.net.wifi.WifiManager; -import android.text.TextUtils; - public class MacAddressUtil { public static String getMacAddress(Context context) { final String rawAddress = getRawMacAddress(context); diff --git a/Adjust/src/com/adjust/sdk/plugin/Plugin.java b/Adjust/adjust/src/main/java/com/adjust/sdk/plugin/Plugin.java similarity index 100% rename from Adjust/src/com/adjust/sdk/plugin/Plugin.java rename to Adjust/adjust/src/main/java/com/adjust/sdk/plugin/Plugin.java diff --git a/Adjust/adjust/src/main/res/values/strings.xml b/Adjust/adjust/src/main/res/values/strings.xml new file mode 100644 index 000000000..5cf65ffdb --- /dev/null +++ b/Adjust/adjust/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + adjust + diff --git a/Adjust/build.gradle b/Adjust/build.gradle index 657d85399..741f70e77 100644 --- a/Adjust/build.gradle +++ b/Adjust/build.gradle @@ -1,48 +1,19 @@ -apply plugin: 'android-library' -buildscript { - repositories { - mavenCentral() - mavenLocal() - } - - dependencies { - classpath 'com.android.tools.build:gradle:+' - } -} +// Top-level build file where you can add configuration options common to all sub-projects/modules. -repositories { - mavenCentral() - mavenLocal() -} - -dependencies { - compile 'com.android.support:support-v4:19.1.+' -} - -android { - buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION - compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) - defaultConfig { - versionCode 11 - versionName '3.6.2' - minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION) - targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) - } - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] +buildscript { + repositories { + jcenter() } - } -} + dependencies { + classpath 'com.android.tools.build:gradle:1.0.1' -task jar(type: Jar) { - from android.sourceSets.main.java + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } } -task wrapper(type: Wrapper) { - gradleVersion = '1.8' +allprojects { + repositories { + jcenter() + } } diff --git a/Adjust/build.xml b/Adjust/build.xml deleted file mode 100644 index 507923cb3..000000000 --- a/Adjust/build.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Adjust/example/.gitignore b/Adjust/example/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/Adjust/example/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Adjust/example/build.gradle b/Adjust/example/build.gradle new file mode 100644 index 000000000..e59d6f944 --- /dev/null +++ b/Adjust/example/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "com.adjust.example" + minSdkVersion 9 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' + compile 'com.google.android.gms:play-services-ads:6.5.87' + // imported module + compile project(":adjust") + // running mvn package + //compile fileTree(dir: '../target', include: ['*.jar']) + // using maven repository + //compile 'com.adjust.sdk:adjust-android:4.0.0' +} diff --git a/Adjust/test/proguard-project.txt b/Adjust/example/proguard-rules.pro similarity index 62% rename from Adjust/test/proguard-project.txt rename to Adjust/example/proguard-rules.pro index f2fe1559a..b88e5ae52 100644 --- a/Adjust/test/proguard-project.txt +++ b/Adjust/example/proguard-rules.pro @@ -1,11 +1,8 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. +# in /Users/pfms/Development/Android_SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/Adjust/example/src/androidTest/java/com/adjust/example/ApplicationTest.java b/Adjust/example/src/androidTest/java/com/adjust/example/ApplicationTest.java new file mode 100644 index 000000000..5d4139ba0 --- /dev/null +++ b/Adjust/example/src/androidTest/java/com/adjust/example/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.adjust.example; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/Adjust/example/src/main/AndroidManifest.xml b/Adjust/example/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b12f001c --- /dev/null +++ b/Adjust/example/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adjust/example/src/main/java/com/adjust/example/GlobalApplication.java b/Adjust/example/src/main/java/com/adjust/example/GlobalApplication.java new file mode 100644 index 000000000..2c83a2480 --- /dev/null +++ b/Adjust/example/src/main/java/com/adjust/example/GlobalApplication.java @@ -0,0 +1,96 @@ +package com.adjust.example; + +import android.app.Application; +import android.util.Log; + +import com.adjust.sdk.Adjust; +import com.adjust.sdk.AdjustAttribution; +import com.adjust.sdk.AdjustConfig; +import com.adjust.sdk.LogLevel; +import com.adjust.sdk.OnAttributionChangedListener; + +/** + * Created by pfms on 17/12/14. + */ +public class GlobalApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + // configure Adjust + String appToken = "{yourAppToken}"; + String environment = AdjustConfig.ENVIRONMENT_SANDBOX; + AdjustConfig config = new AdjustConfig(this, appToken, environment); + + // change the log level + config.setLogLevel(LogLevel.VERBOSE); + + // enable event buffering + //config.setEventBufferingEnabled(true); + + // set default tracker + //config.setDefaultTracker("{YourDefaultTracker}"); + + // set attribution delegate + config.setOnAttributionChangedListener(new OnAttributionChangedListener() { + @Override + public void onAttributionChanged(AdjustAttribution attribution) { + Log.d("example", "attribution: " + attribution.toString()); + } + }); + + Adjust.onCreate(config); + + + // register onResume and onPause events of all activities + // for applications with minimum support of Android v4 or greater + //registerActivityLifecycleCallbacks(new AdjustLifecycleCallbacks()); + + // put the SDK in offline mode + //Adjust.setOfflineMode(true); + + // disable the SDK + //Adjust.setEnabled(false); + } + + // you can use this class if your app is for Android 4.0 or higher + /* + private static final class AdjustLifecycleCallbacks implements ActivityLifecycleCallbacks { + @Override + public void onActivityResumed(Activity activity) { + Adjust.onResume(); + } + + @Override + public void onActivityPaused(Activity activity) { + Adjust.onPause(); + } + + @Override + public void onActivityStopped(Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + + } + + @Override + public void onActivityStarted(Activity activity) { + + } + } + */ + + +} diff --git a/Adjust/example/src/main/java/com/adjust/example/MainActivity.java b/Adjust/example/src/main/java/com/adjust/example/MainActivity.java new file mode 100644 index 000000000..02c32c2e8 --- /dev/null +++ b/Adjust/example/src/main/java/com/adjust/example/MainActivity.java @@ -0,0 +1,93 @@ +package com.adjust.example; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import com.adjust.sdk.Adjust; +import com.adjust.sdk.AdjustEvent; + +public class MainActivity extends ActionBarActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Intent intent = getIntent(); + Uri data = intent.getData(); + Adjust.appWillOpenUrl(data); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, 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 == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onResume() { + super.onResume(); + Adjust.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + Adjust.onPause(); + } + + public void onTrackSimpleEventClick(View v) { + AdjustEvent event = new AdjustEvent("{eventToken}"); + + Adjust.trackEvent(event); + } + + public void onTrackRevenueEventClick(View v) { + AdjustEvent event = new AdjustEvent("{eventToken}"); + + // add revenue 1 cent of an euro + event.setRevenue(0.01, "EUR"); + + Adjust.trackEvent(event); + } + + public void onTrackEventWithCallbackClick(View v) { + AdjustEvent event = new AdjustEvent("{eventToken}"); + + // add callback parameters to this parameter + event.addCallbackParameter("key", "value"); + + Adjust.trackEvent(event); + } + + public void onTrackEventWithPartnerClick(View v) { + AdjustEvent event = new AdjustEvent("{eventToken}"); + + // add partner parameters to this parameter + event.addPartnerParameter("foo", "bar"); + + Adjust.trackEvent(event); + } +} diff --git a/Adjust/example/src/main/res/layout/activity_main.xml b/Adjust/example/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..ecab379a0 --- /dev/null +++ b/Adjust/example/src/main/res/layout/activity_main.xml @@ -0,0 +1,57 @@ + + + + +