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