diff --git a/.gitignore b/.gitignore index 7d85933f8..e5b66406c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ gen/ local.properties *.remote + +AdjustIo/.classpath +AdjustIo/.project +AdjustIo/.settings diff --git a/AdjustIo/src/com/adeven/adjustio/ActivityHandler.java b/AdjustIo/src/com/adeven/adjustio/ActivityHandler.java new file mode 100644 index 000000000..3393f9299 --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/ActivityHandler.java @@ -0,0 +1,480 @@ +// +// ActivityHandler.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +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.Date; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +public class ActivityHandler extends HandlerThread { + private static final String SESSION_STATE_FILENAME = "AdjustIoActivityState"; + + private static final long TIMER_INTERVAL = 1000 * 60 * 1; // 1 minute + private static final long SESSION_INTERVAL = 1000 * 60 * 30; // 30 minutes + private static final long SUBSESSION_INTERVAL = 1000 * 1; // one second + + private InternalHandler internalHandler; + private PackageHandler packageHandler; + private ActivityState activityState; + private static ScheduledExecutorService timer; + private Context context; + + private String appToken; + private String macSha1; + private String macShortMd5; + private String androidId; // everything else here could be persisted + private String fbAttributionId; + private String userAgent; // changes, should be updated periodically + private String clientSdk; + + protected ActivityHandler(String appToken, Context context) { + super(Logger.LOGTAG, MIN_PRIORITY); + setDaemon(true); + start(); + internalHandler = new InternalHandler(getLooper(), this); + + this.context = context; + + Message message = Message.obtain(); + message.arg1 = InternalHandler.INIT; + message.obj = appToken; + internalHandler.sendMessage(message); + } + + protected void trackSubsessionStart() { + Message message = Message.obtain(); + message.arg1 = InternalHandler.START; + internalHandler.sendMessage(message); + } + + protected void trackSubsessionEnd() { + Message message = Message.obtain(); + message.arg1 = InternalHandler.END; + internalHandler.sendMessage(message); + } + + protected void trackEvent(String eventToken, Map parameters) { + PackageBuilder builder = new PackageBuilder(); + builder.eventToken = eventToken; + builder.callbackParameters = parameters; + + Message message = Message.obtain(); + message.arg1 = InternalHandler.EVENT; + message.obj = builder; + internalHandler.sendMessage(message); + } + + protected void trackRevenue(double amountInCents, String eventToken, Map parameters) { + PackageBuilder builder = new PackageBuilder(); + builder.amountInCents = amountInCents; + builder.eventToken = eventToken; + builder.callbackParameters = parameters; + + Message message = Message.obtain(); + message.arg1 = InternalHandler.REVENUE; + message.obj = builder; + internalHandler.sendMessage(message); + } + + private static final class InternalHandler extends Handler { + private static final int INIT = 72630; + private static final int START = 72640; + private static final int END = 72650; + private static final int EVENT = 72660; + private static final int REVENUE = 72670; + + private final WeakReference sessionHandlerReference; + + protected InternalHandler(Looper looper, ActivityHandler sessionHandler) { + super(looper); + this.sessionHandlerReference = new WeakReference(sessionHandler); + } + + public void handleMessage(Message message) { + super.handleMessage(message); + + ActivityHandler sessionHandler = sessionHandlerReference.get(); + if (sessionHandler == null) return; + + switch (message.arg1) { + case INIT: + String appToken = (String) message.obj; + sessionHandler.initInternal(appToken); + break; + case START: + sessionHandler.startInternal(); + break; + case END: + sessionHandler.endInternal(); + break; + case EVENT: + PackageBuilder eventBuilder = (PackageBuilder) message.obj; + sessionHandler.eventInternal(eventBuilder); + break; + case REVENUE: + PackageBuilder revenueBuilder = (PackageBuilder) message.obj; + sessionHandler.revenueInternal(revenueBuilder); + break; + } + } + } + + private void initInternal(String token) { + if (!checkAppTokenNotNull(token)) return; + if (!checkAppTokenLength(token)) return; + if (!checkContext(context)) return; + if (!checkPermissions(context)) return; + + String macAddress = Util.getMacAddress(context); + String macShort = macAddress.replaceAll(":", ""); + + appToken = token; + macSha1 = Util.sha1(macAddress); + macShortMd5 = Util.md5(macShort); + androidId = Util.getAndroidId(context); + fbAttributionId = Util.getAttributionId(context); + userAgent = Util.getUserAgent(context); + clientSdk = Util.CLIENT_SDK; + + packageHandler = new PackageHandler(context); + readActivityState(); + } + + private void startInternal() { + if (!checkAppTokenNotNull(appToken)) return; + + packageHandler.resumeSending(); + startTimer(); + + long now = new Date().getTime(); + + // very first session + if (activityState == null) { + activityState = new ActivityState(); + activityState.sessionCount = 1; // this is the first session + activityState.createdAt = now; // starting now + + transferSessionPackage(); + activityState.resetSessionAttributes(now); + writeActivityState(); + Logger.info("First session"); + 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.createdAt = now; + activityState.lastInterval = lastInterval; + + transferSessionPackage(); + activityState.resetSessionAttributes(now); + writeActivityState(); + Logger.debug(String.format(Locale.US, + "Session %d", activityState.sessionCount)); + return; + } + + // new subsession + if (lastInterval > SUBSESSION_INTERVAL) { + activityState.subsessionCount++; + Logger.info(String.format(Locale.US, + "Started subsession %d of session %d", + activityState.subsessionCount, + activityState.sessionCount)); + } + activityState.sessionLength += lastInterval; + activityState.lastActivity = now; + writeActivityState(); + } + + private void endInternal() { + if (!checkAppTokenNotNull(appToken)) return; + + packageHandler.pauseSending(); + stopTimer(); + updateActivityState(); + writeActivityState(); + } + + private void eventInternal(PackageBuilder eventBuilder) { + if (!checkAppTokenNotNull(appToken)) return; + if (!checkActivityState(activityState)) return; + if (!checkEventTokenNotNull(eventBuilder.eventToken)) return; + if (!checkEventTokenLength(eventBuilder.eventToken)) return; + + long now = new Date().getTime(); + activityState.createdAt = now; + activityState.eventCount++; + updateActivityState(); + + injectGeneralAttributes(eventBuilder); + activityState.injectEventAttributes(eventBuilder); + ActivityPackage eventPackage = eventBuilder.buildEventPackage(); + packageHandler.addPackage(eventPackage); + + writeActivityState(); + Logger.debug(String.format(Locale.US, "Event %d", activityState.eventCount)); + } + + + private void revenueInternal(PackageBuilder revenueBuilder) { + if (!checkAppTokenNotNull(appToken)) return; + if (!checkActivityState(activityState)) return; + if (!checkAmount(revenueBuilder.amountInCents)) return; + if (!checkEventTokenLength(revenueBuilder.eventToken)) return; + + long now = new Date().getTime(); + activityState.createdAt = now; + activityState.eventCount++; + updateActivityState(); + + injectGeneralAttributes(revenueBuilder); + activityState.injectEventAttributes(revenueBuilder); + ActivityPackage eventPackage = revenueBuilder.buildRevenuePackage(); + packageHandler.addPackage(eventPackage); + + writeActivityState(); + Logger.debug(String.format(Locale.US, "Event %d (revenue)", activityState.eventCount)); + } + + private void updateActivityState() { + if (!checkActivityState(activityState)) return; + + long now = new Date().getTime(); + long lastInterval = now - activityState.lastActivity; + if (lastInterval < 0) { + Logger.error("Time travel!"); + activityState.lastActivity = now; + return; + } + + // ignore late updates + if (lastInterval > SESSION_INTERVAL) return; + + activityState.sessionLength += lastInterval; + activityState.timeSpent += lastInterval; + activityState.lastActivity = now; + } + + private void readActivityState() { + try { + FileInputStream inputStream = context.openFileInput(SESSION_STATE_FILENAME); + BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); + ObjectInputStream objectStream = new ObjectInputStream(bufferedStream); + + try { + activityState = (ActivityState) objectStream.readObject(); + Logger.debug(String.format("Read activity state: %s", activityState)); + return; + } + catch (ClassNotFoundException e) { + Logger.error("Failed to find activity state class"); + } + catch (OptionalDataException e) {} + catch (IOException e) { + Logger.error("Failed to read activity states object"); + } + catch (ClassCastException e) { + Logger.error("Failed to cast activity state object"); + } + finally { + objectStream.close(); + } + + } + catch (FileNotFoundException e) { + Logger.verbose("Activity state file not found"); + } + catch (IOException e) { + Logger.error("Failed to read activity state file"); + } + + // start with a fresh activity state in case of any exception + activityState = null; + } + + private void writeActivityState() { + try { + FileOutputStream outputStream = context.openFileOutput(SESSION_STATE_FILENAME, Context.MODE_PRIVATE); + BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream); + ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream); + + try { + objectStream.writeObject(activityState); + Logger.verbose(String.format("Wrote activity state: %s", activityState)); + } + catch (NotSerializableException e) { + Logger.error("Failed to serialize activity state"); + } + finally { + objectStream.close(); + } + + } + catch (IOException e) { + Logger.error(String.format("Failed to write activity state (%s)", e)); + } + } + + private void transferSessionPackage() { + PackageBuilder builder = new PackageBuilder(); + injectGeneralAttributes(builder); + activityState.injectSessionAttributes(builder); + ActivityPackage sessionPackage = builder.buildSessionPackage(); + packageHandler.addPackage(sessionPackage); + } + + private void injectGeneralAttributes(PackageBuilder builder) { + builder.appToken = appToken; + builder.macShortMd5 = macShortMd5; + builder.macSha1 = macSha1; + builder.androidId = androidId; + builder.fbAttributionId = fbAttributionId; + builder.userAgent = userAgent; + builder.clientSdk = clientSdk; + } + + private void startTimer() { + if (timer != null) { + stopTimer(); + } + timer = Executors.newSingleThreadScheduledExecutor(); + timer.scheduleWithFixedDelay(new Runnable() { + public void run() { + timerFired(); + } + }, 1000, TIMER_INTERVAL, TimeUnit.MILLISECONDS); + } + + private void stopTimer() { + try { + timer.shutdown(); + } + catch (NullPointerException e) { + Logger.error("No timer found"); + } + } + + private void timerFired() { + packageHandler.sendFirstPackage(); + + updateActivityState(); + writeActivityState(); + } + + private static boolean checkPermissions(Context context) { + boolean result = true; + + if (!checkPermission(context, android.Manifest.permission.INTERNET)) { + Logger.error("Missing permission: INTERNET"); + result = false; + } + if (!checkPermission(context, android.Manifest.permission.ACCESS_WIFI_STATE)) { + Logger.warn("Missing permission: ACCESS_WIFI_STATE"); + } + + return result; + } + + private static boolean checkContext(Context context) { + if (context == null) { + Logger.error("Missing context"); + return false; + } + return true; + } + + private static boolean checkPermission(Context context, String permission) { + int result = context.checkCallingOrSelfPermission(permission); + boolean granted = (result == PackageManager.PERMISSION_GRANTED); + return granted; + } + + private static boolean checkActivityState(ActivityState activityState) { + if (activityState == null) { + Logger.error("Missing activity state."); + return false; + } + return true; + } + + private static boolean checkAppTokenNotNull(String appToken) { + if (appToken == null) { + Logger.error("Missing App Token."); + return false; + } + return true; + } + + private static boolean checkAppTokenLength(String appToken) { + if (appToken.length() != 12) { + Logger.error(String.format("Malformed App Token '%s'", appToken)); + return false; + } + return true; + } + + private static boolean checkEventTokenNotNull(String eventToken) { + if (eventToken == null) { + Logger.error("Missing Event Token"); + return false; + } + return true; + } + + private static boolean checkEventTokenLength(String eventToken) { + if (eventToken == null) + return true; + + if (eventToken.length() != 6) { + Logger.error(String.format("Malformed Event Token '%s'", eventToken)); + return false; + } + return true; + } + + private static boolean checkAmount(double amount) { + if (amount <= 0.0) { + Logger.error(String.format(Locale.US, "Invalid amount %f", amount)); + return false; + } + return true; + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/ActivityPackage.java b/AdjustIo/src/com/adeven/adjustio/ActivityPackage.java new file mode 100644 index 000000000..2735a308b --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/ActivityPackage.java @@ -0,0 +1,54 @@ +// +// ActivityPackage.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +import java.io.Serializable; +import java.util.Map; + +public class ActivityPackage implements Serializable { + private static final long serialVersionUID = -35935556512024097L; + + // data + protected String path; + protected String userAgent; + protected String clientSdk; + protected Map parameters; + + // logs + protected String kind; + protected String suffix; + + public String toString() { + return String.format("%s%s", kind, suffix); + } + + protected String getExtendedString() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("Path: %s\n", path)); + builder.append(String.format("UserAgent: %s\n", userAgent)); + builder.append(String.format("ClientSdk: %s\n", clientSdk)); + + if (parameters != null) { + builder.append("Parameters:"); + for (Map.Entry entity : parameters.entrySet()) { + builder.append(String.format("\n\t%-16s %s", entity.getKey(), entity.getValue())); + } + } + return builder.toString(); + } + + protected String getSuccessMessage() { + return String.format("Tracked %s%s", kind, suffix); + } + + protected String getFailureMessage() { + return String.format("Failed to track %s%s", kind, suffix); + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/ActivityState.java b/AdjustIo/src/com/adeven/adjustio/ActivityState.java new file mode 100644 index 000000000..8a94baf31 --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/ActivityState.java @@ -0,0 +1,86 @@ +// +// ActivityState.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +import java.io.Serializable; +import java.util.Date; +import java.util.Locale; + +public class ActivityState implements Serializable { + private static final long serialVersionUID = 9039439291143138148L; + + // 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 createdAt; + protected long lastInterval; + + protected ActivityState() { + eventCount = 0; // no events yet + sessionCount = 0; // the first session just started + subsessionCount = -1; // we don't know how many subssessions 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; + createdAt = -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; + createdAt = -1; + lastInterval = -1; + } + + protected void injectSessionAttributes(PackageBuilder builder) { + injectGeneralAttributes(builder); + builder.lastInterval = lastInterval; + } + + protected void injectEventAttributes(PackageBuilder builder) { + injectGeneralAttributes(builder); + builder.eventCount = eventCount; + } + + public String toString() { + return String.format(Locale.US, + "ec:%d sc:%d ssc:%d sl:%.1f ts:%.1f la:%s", + eventCount, sessionCount, subsessionCount, + sessionLength / 1000.0, timeSpent / 1000.0, + stamp(lastActivity)); + } + + private static String stamp(long dateMillis) { + Date date = new Date(dateMillis); + return String.format(Locale.US, + "%02d:%02d:%02d", + date.getHours(), + date.getMinutes(), + date.getSeconds()); + } + + private void injectGeneralAttributes(PackageBuilder builder) { + builder.sessionCount = sessionCount; + builder.subsessionCount = subsessionCount; + builder.sessionLength = sessionLength; + builder.timeSpent = timeSpent; + builder.createdAt = createdAt; + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/AdjustIo.java b/AdjustIo/src/com/adeven/adjustio/AdjustIo.java index 4d2c5fdac..1ba632226 100644 --- a/AdjustIo/src/com/adeven/adjustio/AdjustIo.java +++ b/AdjustIo/src/com/adeven/adjustio/AdjustIo.java @@ -2,7 +2,7 @@ // AdjustIo.java // AdjustIo // -// Created by Christian Wellenbrock on 11.10.12. +// Created by Christian Wellenbrock on 2012-10-11. // Copyright (c) 2012 adeven. All rights reserved. // See the file MIT-LICENSE for copying permission. // @@ -11,59 +11,61 @@ import java.util.Map; -import android.content.Context; +import android.app.Activity; /** * The main interface to AdjustIo. * * Use the methods of this class to tell AdjustIo about the usage of your app. * See the README for details. - * - * @author wellle - * @since 11.10.12 */ public class AdjustIo { /** - * Tell AdjustIo that the application did launch. + * Tell AdjustIo that an activity did resume. * - * This is required to initialize AdjustIo. - * Call this in the onCreate method of your launch activity. + * This is used to initialize AdjustIo and keep track of the current session state. + * Call this in the onResume method of every activity of your app. * - * @param context Your application context - * Generally obtained by calling getApplication() + * @param appToken The App Token for your app. This unique identifier can + * be found in your dashboard at http://adjust.io and should always + * be 12 characters long. + * @param activity The activity that has just resumed. */ - public static void appDidLaunch(Context context) { - if (!Util.checkPermissions(context)) { - return; + public static void onResume(String appToken, Activity activity) { + if (activityHandler == null) { + activityHandler = new ActivityHandler(appToken, activity); } - - String macAddress = Util.getMacAddress(context); - - packageName = context.getPackageName(); - macSha1 = Util.sha1(macAddress); - macShort = macAddress.replaceAll(":", ""); - userAgent = Util.getUserAgent(context); - androidId = Util.getAndroidId(context); - attributionId = Util.getAttributionId(context); - - trackSessionStart(); + activityHandler.trackSubsessionStart(); } + /** + * Tell AdjustIo that an activity will pause. + * + * This is used to calculate session attributes like session length and subsession count. + * Call this in the onPause method of every activity of your app. + */ + public static void onPause() { + try { + activityHandler.trackSubsessionEnd(); + } catch (NullPointerException e) { + Logger.error("No activity handler found"); + } + } /** - * Track any kind of event. + * Tell AdjustIo that a particular event has happened. * - * You can assign a callback url to the event which - * will get called every time the event is reported. You can also provide - * parameters that will be forwarded to these callbacks. + * In your dashboard at http://adjust.io you can assign a callback URL to each + * event type. That URL will get called every time the event is triggered. On + * top of that you can pass a set of parameters to the following method that + * will be forwarded to these callbacks. * - * @param eventToken The token for this kind of event - * It must be exactly six characters long - * You create them in your dashboard at http://www.adjust.io - * @param parameters An optional dictionary containing callback parameters - * Provide key-value-pairs to be forwarded to your callbacks + * @param eventToken The Event Token for this kind of event. They are created + * in the dashboard at http://adjust.io and should be six characters long. + * @param parameters An optional dictionary containing the callback parameters. + * Provide key-value-pairs to be forwarded to your callbacks. */ public static void trackEvent(String eventToken) { @@ -71,102 +73,55 @@ public static void trackEvent(String eventToken) { } public static void trackEvent(String eventToken, Map parameters) { - if (eventToken.length() != 6) { - Logger.error( - "Event tracking only works with proper event tokens. " + - "Find them in your dashboard at http://www.adjust.io " + - "or contact support@adjust.io" - ); - return; + try { + activityHandler.trackEvent(eventToken, parameters); + } catch (NullPointerException e) { + Logger.error("No activity handler found"); } - - String paramString = Util.getBase64EncodedParameters(parameters); - String successMessage = "Tracked event: '" + eventToken + "'"; - String failureMessage = "Failed to track event: '" + eventToken + "'"; - - TrackingPackage event = new TrackingPackage.Builder() - .setPath("/event") - .setSuccessMessage(successMessage) - .setFailureMessage(failureMessage) - .setUserAgent(userAgent) - .addTrackingParameter(EVENT_TOKEN, eventToken) - .addTrackingParameter(PACKAGE_NAME, packageName) - .addTrackingParameter(MAC_SHORT, macShort) - .addTrackingParameter(ANDROID_ID, androidId) - .addTrackingParameter(PARAMETERS, paramString) - .build(); - getRequestThread().track(event); } /** - * Tell AdjustIo that the current user generated some revenue. + * Tell AdjustIo that a user generated some revenue. * - * The amount is measured in cents and rounded to on digit after the decimal - * point. If you want to differentiate between various types of specific revenues - * you can do so by using different event tokens. If your revenue events have - * callbacks, you can also pass in parameters that will be forwarded to your - * server. + * The amount is measured in cents and rounded to on digit after the + * decimal point. If you want to differentiate between several revenue + * types, you can do so by using different event tokens. If your revenue + * events have callbacks, you can also pass in parameters that will be + * forwarded to your end point. * - * @param amountInCents The amount in cents (example: 1.5f means one and a half cents) - * @param eventToken The token for this revenue event (see above) - * @param parameters Parameters for this revenue event (see above) + * @param amountInCents The amount in cents (example: 1.5 means one and a half cents) + * @param eventToken The token for this revenue event (optional, see above) + * @param parameters Parameters for this revenue event (optional, see above) */ - public static void trackRevenue(float amountInCents) { + public static void trackRevenue(double amountInCents) { AdjustIo.trackRevenue(amountInCents, null); } - public static void trackRevenue(float amountInCents, String eventToken) { + public static void trackRevenue(double amountInCents, String eventToken) { AdjustIo.trackRevenue(amountInCents, eventToken, null); } - public static void trackRevenue(float amountInCents, String eventToken, Map parameters) { - if (eventToken != null && eventToken.length() != 6) { - Logger.error( - "Specific revenue tracking only works with proper event tokens. " + - "Find them in your dashboard at http://www.adjust.io " + - "or contact support@adjust.io" - ); - return; + public static void trackRevenue(double amountInCents, String eventToken, Map parameters) { + try { + activityHandler.trackRevenue(amountInCents, eventToken, parameters); + } catch (NullPointerException e) { + Logger.error("No activity handler found"); } - - int amountInMillis = Math.round(10 * amountInCents); - amountInCents = amountInMillis/10.0f; // now rounded to one decimal point - String amount = Integer.toString(amountInMillis); - String paramString = Util.getBase64EncodedParameters(parameters); - String successMessage = "Tracked revenue: " + amountInCents + " Cent"; - String failureMessage = "Failed to track revenue: " + amountInCents + " Cent"; - - if (eventToken != null) { - String eventString = " (event token: '" + eventToken + "')"; - successMessage += eventString; - failureMessage += eventString; - } - - TrackingPackage revenue = new TrackingPackage.Builder() - .setPath("/revenue") - .setSuccessMessage(successMessage) - .setFailureMessage(failureMessage) - .setUserAgent(userAgent) - .addTrackingParameter(PACKAGE_NAME, packageName) - .addTrackingParameter(MAC_SHORT, macShort) - .addTrackingParameter(ANDROID_ID, androidId) - .addTrackingParameter(AMOUNT, amount) - .addTrackingParameter(EVENT_TOKEN, eventToken) - .addTrackingParameter(PARAMETERS, paramString) - .build(); - getRequestThread().track(revenue); } /** * Change the verbosity of AdjustIo's logs. * + * You can increase or reduce the amount of logs from AdjustIo by passing + * one of the following parameters. Use Log.ASSERT to disable all logging. + * * @param logLevel The desired minimum log level (default: info) * Must be one of the following: * - Log.VERBOSE (enable all logging) - * - Log.DEBUG + * - Log.DEBUG (enable more logging) * - Log.INFO (the default) * - Log.WARN (disable info logging) * - Log.ERROR (disable warnings as well) @@ -178,44 +133,9 @@ public static void setLogLevel(int logLevel) { } - // This line marks the end of the public interface. - - private static final String PACKAGE_NAME = "app_id"; - private static final String MAC_SHA1 = "mac_sha1"; - private static final String MAC_SHORT = "mac"; - private static final String ANDROID_ID = "android_id"; - private static final String ATTRIBUTION_ID = "fb_id"; - private static final String EVENT_TOKEN = "event_id"; - private static final String PARAMETERS = "params"; - private static final String AMOUNT = "amount"; - - private static String packageName; - private static String macSha1; - private static String macShort; - private static String userAgent; - private static String androidId; - private static String attributionId; - - private static void trackSessionStart() { - TrackingPackage sessionStart = new TrackingPackage.Builder() - .setPath("/startup") - .setSuccessMessage("Tracked session start.") - .setFailureMessage("Failed to track session start.") - .setUserAgent(userAgent) - .addTrackingParameter(PACKAGE_NAME, packageName) - .addTrackingParameter(MAC_SHORT, macShort) - .addTrackingParameter(MAC_SHA1, macSha1) - .addTrackingParameter(ANDROID_ID, androidId) - .addTrackingParameter(ATTRIBUTION_ID, attributionId) - .build(); - getRequestThread().track(sessionStart); - } + /** + * Every activity will get forwarded to this handler to be processed in the background. + */ + private static ActivityHandler activityHandler; - private static RequestThread requestThread; - private static RequestThread getRequestThread() { - if (requestThread == null) { - requestThread = new RequestThread(); - } - return requestThread; - } } diff --git a/AdjustIo/src/com/adeven/adjustio/Logger.java b/AdjustIo/src/com/adeven/adjustio/Logger.java index 6f13af311..fbf11e83e 100644 --- a/AdjustIo/src/com/adeven/adjustio/Logger.java +++ b/AdjustIo/src/com/adeven/adjustio/Logger.java @@ -2,8 +2,8 @@ // Logger.java // AdjustIo // -// Created by Benjamin Weiss on 17.4.13 -// Copyright (c) 2012 adeven. All rights reserved. +// Created by Christian Wellenbrock on 2013-04-18. +// Copyright (c) 2013 adeven. All rights reserved. // See the file MIT-LICENSE for copying permission. // @@ -11,58 +11,42 @@ import android.util.Log; -/** - * A Wrapper that allows easy toggles of Logging. - * - * @author keyboardsurfer - * @since 17.4.13 - */ public class Logger { protected static final String LOGTAG = "AdjustIo"; private static int logLevel = Log.INFO; - public static void setLogLevel(int logLevel) { + protected static void setLogLevel(int logLevel) { Logger.logLevel = logLevel; } - public static void verbose(String message) { + protected static void verbose(String message) { if (logLevel <= Log.VERBOSE) { Log.v(LOGTAG, message); } } - public static void verbose(String context, String name, String value) { - verbose("[" + context + "] " + name + ": '" + value + "'"); - } - - public static void debug(String message) { + protected static void debug(String message) { if (logLevel <= Log.DEBUG) { Log.d(LOGTAG, message); } } - public static void info(String message) { + protected static void info(String message) { if (logLevel <= Log.INFO) { Log.i(LOGTAG, message); } } - public static void warn(String message) { + protected static void warn(String message) { if (logLevel <= Log.WARN) { Log.w(LOGTAG, message); } } - public static void error(String message) { + protected static void error(String message) { if (logLevel <= Log.ERROR) { Log.e(LOGTAG, message); } } - - public static void error(String message, Throwable throwable) { - if (logLevel <= Log.ERROR) { - Log.e(LOGTAG, message, throwable); - } - } } diff --git a/AdjustIo/src/com/adeven/adjustio/PackageBuilder.java b/AdjustIo/src/com/adeven/adjustio/PackageBuilder.java new file mode 100644 index 000000000..199bd18bc --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/PackageBuilder.java @@ -0,0 +1,187 @@ +// +// PackageBuilder.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.json.JSONObject; + +import android.util.Base64; + +public class PackageBuilder { + + private static String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'Z"; + + // general + protected String appToken; + protected String macSha1; + protected String macShortMd5; + protected String androidId; + protected String fbAttributionId; + protected String userAgent; + protected String clientSdk; + + // sessions + protected int sessionCount; + protected int subsessionCount; + protected long createdAt; + protected long sessionLength; + protected long timeSpent; + protected long lastInterval; + + // events + protected int eventCount; + protected String eventToken; + protected double amountInCents; + protected Map callbackParameters; + + private static SimpleDateFormat dateFormat; + + protected ActivityPackage buildSessionPackage() { + Map parameters = getDefaultParameters(); + addDuration(parameters, "last_interval", lastInterval); + + ActivityPackage sessionPackage = getDefaultActivityPackage(); + sessionPackage.path = "/startup"; + sessionPackage.kind = "session start"; + sessionPackage.suffix = ""; + sessionPackage.parameters = parameters; + + return sessionPackage; + } + + protected ActivityPackage buildEventPackage() { + Map parameters = getDefaultParameters(); + injectEventParameters(parameters); + + ActivityPackage eventPackage = getDefaultActivityPackage(); + eventPackage.path = "/event"; + eventPackage.kind = "event"; + eventPackage.suffix = getEventSuffix(); + eventPackage.parameters = parameters; + + return eventPackage; + } + + protected ActivityPackage buildRevenuePackage() { + Map parameters = getDefaultParameters(); + injectEventParameters(parameters); + addString(parameters, "amount", getAmountString()); + + ActivityPackage revenuePackage = getDefaultActivityPackage(); + revenuePackage.path = "/revenue"; + revenuePackage.kind = "revenue"; + revenuePackage.suffix = getRevenueSuffix(); + revenuePackage.parameters = parameters; + + return revenuePackage; + } + + private ActivityPackage getDefaultActivityPackage() { + ActivityPackage activityPackage = new ActivityPackage(); + activityPackage.userAgent = userAgent; + activityPackage.clientSdk = clientSdk; + return activityPackage; + } + + private Map getDefaultParameters() { + Map parameters = new HashMap(); + + // general + addDate(parameters, "created_at", createdAt); + addString(parameters, "app_token", appToken); + addString(parameters, "mac_sha1", macSha1); + addString(parameters, "mac_md5", macShortMd5); + addString(parameters, "android_id", androidId); + addString(parameters, "fb_id", fbAttributionId); + + // session related (used for events as well) + addInt(parameters, "session_count", sessionCount); + addInt(parameters, "subsession_count", subsessionCount); + addDuration(parameters, "session_length", sessionLength); + addDuration(parameters, "time_spent", timeSpent); + + return parameters; + } + + private void injectEventParameters(Map parameters) { + addInt(parameters, "event_count", eventCount); + addString(parameters, "event_token", eventToken); + addMap(parameters, "params", callbackParameters); + } + + private String getAmountString() { + long amountInMillis = Math.round(10 * amountInCents); + amountInCents = amountInMillis / 10.0; // now rounded to one decimal point + String amountString = Long.toString(amountInMillis); + return amountString; + } + + private String getEventSuffix() { + return String.format(" '%s'", eventToken); + } + + private String getRevenueSuffix() { + if (eventToken != null) { + return String.format(Locale.US, " (%.1f cent, '%s')", amountInCents, eventToken); + } else { + return String.format(Locale.US, " (%.1f cent)", amountInCents); + } + } + + private void addString(Map parameters, String key, String value) { + if (value == null || value == "") return; + + parameters.put(key, value); + } + + private void addInt(Map parameters, String key, long value) { + if (value < 0) return; + + String valueString = Long.toString(value); + addString(parameters, key, valueString); + } + + private void addDate(Map parameters, String key, long value) { + if (value < 0) return; + + Date date = new Date(value); + String dateString = getDateFormat().format(date); + addString(parameters, key, dateString); + } + + private void addDuration(Map parameters, String key, long durationInMilliSeconds) { + if (durationInMilliSeconds < 0) return; + + long durationInSeconds = (durationInMilliSeconds + 500) / 1000; + addInt(parameters, key, durationInSeconds); + } + + private void addMap(Map parameters, String key, Map map) { + if (map == null) return; + + JSONObject jsonObject = new JSONObject(map); + byte[] jsonBytes = jsonObject.toString().getBytes(); + String encodedMap = Base64.encodeToString(jsonBytes, Base64.NO_WRAP); + + addString(parameters, key, encodedMap); + } + + private SimpleDateFormat getDateFormat() { + if (dateFormat == null) { + dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); + } + return dateFormat; + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/PackageHandler.java b/AdjustIo/src/com/adeven/adjustio/PackageHandler.java new file mode 100644 index 000000000..06a05142e --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/PackageHandler.java @@ -0,0 +1,234 @@ +// +// PackageHandler.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +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.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +// persistent +public class PackageHandler extends HandlerThread { + private static final String PACKAGE_QUEUE_FILENAME = "AdjustIoPackageQueue"; + + private InternalHandler internalHandler; + private RequestHandler requestHandler; + private List packageQueue; + private AtomicBoolean isSending; + private boolean paused; + private Context context; + + protected PackageHandler(Context context) { + super(Logger.LOGTAG, MIN_PRIORITY); + setDaemon(true); + start(); + this.internalHandler = new InternalHandler(getLooper(), this); + + this.context = context; + + Message message = Message.obtain(); + message.arg1 = InternalHandler.INIT; + internalHandler.sendMessage(message); + } + + // add a package to the queue, trigger sending + protected void addPackage(ActivityPackage pack) { + Message message = Message.obtain(); + message.arg1 = InternalHandler.ADD; + message.obj = pack; + internalHandler.sendMessage(message); + } + + // try to send the oldest package + protected 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) + protected 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) + protected void closeFirstPackage() { + isSending.set(false); + } + + // interrupt the sending loop after the current request has finished + protected void pauseSending() { + paused = true; + } + + // allow sending requests again + protected void resumeSending() { + paused = false; + } + + 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 packageHandlerReference; + + protected InternalHandler(Looper looper, PackageHandler packageHandler) { + super(looper); + this.packageHandlerReference = new WeakReference(packageHandler); + } + + public void handleMessage(Message message) { + super.handleMessage(message); + + PackageHandler packageHandler = packageHandlerReference.get(); + if (packageHandler == null) 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 = new RequestHandler(this); + isSending = new AtomicBoolean(); + + readPackageQueue(); + } + + private void addInternal(ActivityPackage newPackage) { + packageQueue.add(newPackage); + Logger.debug(String.format(Locale.US, "Added package %d (%s)", packageQueue.size(), newPackage)); + Logger.verbose(newPackage.getExtendedString()); + + writePackageQueue(); + sendFirstInternal(); + } + + private void sendFirstInternal() { + if (packageQueue.size() == 0) 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 packageQueue = (List) object; + Logger.debug(String.format(Locale.US, "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) {} 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 (IOException e) { + Logger.error("Failed to read package queue file"); + } + + // start with a fresh package queue in case of any exception + packageQueue = new ArrayList(); + } + + 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(String.format(Locale.US, "Package handler wrote %d packages", packageQueue.size())); + } + catch (NotSerializableException e) { + Logger.error("Failed to serialize packages"); + } + finally { + objectStream.close(); + } + } + catch (IOException e) { + Logger.error(String.format("Failed to write packages (%s)", e.getLocalizedMessage())); + e.printStackTrace(); + } + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/RequestHandler.java b/AdjustIo/src/com/adeven/adjustio/RequestHandler.java new file mode 100644 index 000000000..ca63baf5f --- /dev/null +++ b/AdjustIo/src/com/adeven/adjustio/RequestHandler.java @@ -0,0 +1,199 @@ +// +// RequestHandler.java +// AdjustIo +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adeven. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adeven.adjustio; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.ref.WeakReference; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +public class RequestHandler extends HandlerThread { + private static final int CONNECTION_TIMEOUT = 1000 * 60 * 1; // 1 minute + private static final int SOCKET_TIMEOUT = 1000 * 60 * 1; // 1 minute + + private InternalHandler internalHandler; + private PackageHandler packageHandler; + private HttpClient httpClient; + + protected RequestHandler(PackageHandler packageHandler) { + super(Logger.LOGTAG, MIN_PRIORITY); + setDaemon(true); + start(); + + this.internalHandler = new InternalHandler(getLooper(), this); + this.packageHandler = packageHandler; + + Message message = Message.obtain(); + message.arg1 = InternalHandler.INIT; + internalHandler.sendMessage(message); + } + + protected void sendPackage(ActivityPackage pack) { + Message message = Message.obtain(); + message.arg1 = InternalHandler.SEND; + message.obj = pack; + internalHandler.sendMessage(message); + } + + private static final class InternalHandler extends Handler { + private static final int INIT = 72401; + private static final int SEND = 72400; + + private final WeakReference requestHandlerReference; + + protected InternalHandler(Looper looper, RequestHandler requestHandler) { + super(looper); + this.requestHandlerReference = new WeakReference(requestHandler); + } + + public void handleMessage(Message message) { + super.handleMessage(message); + + RequestHandler requestHandler = requestHandlerReference.get(); + if (requestHandler == null) return; + + switch (message.arg1) { + case INIT: + requestHandler.initInternal(); + break; + case SEND: + ActivityPackage activityPackage = (ActivityPackage) message.obj; + requestHandler.sendInternal(activityPackage); + break; + } + } + } + + private void initInternal() { + HttpParams httpParams = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParams, CONNECTION_TIMEOUT); + HttpConnectionParams.setSoTimeout(httpParams, SOCKET_TIMEOUT); + httpClient = new DefaultHttpClient(httpParams); + } + + private void sendInternal(ActivityPackage activityPackage) { + try { + HttpUriRequest request = getRequest(activityPackage); + HttpResponse response = httpClient.execute(request); + requestFinished(response, activityPackage); + } + catch (UnsupportedEncodingException e) { + sendNextPackage(activityPackage, "Failed to encode parameters", e); + } + catch (ClientProtocolException e) { + closePackage(activityPackage, "Client protocol error", e); + } + catch (SocketTimeoutException e) { + closePackage(activityPackage, "Request timed out", e); + } + catch (IOException e) { + closePackage(activityPackage, "Request failed", e); + } + catch (Exception e) { + sendNextPackage(activityPackage, "Runtime exeption", e); + } + } + + private void requestFinished(HttpResponse response, ActivityPackage activityPackage) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseString = parseResponse(response); + + if (statusCode == HttpStatus.SC_OK) { + Logger.info(activityPackage.getSuccessMessage()); + } else { + Logger.error(String.format("%s. (%s)", activityPackage.getFailureMessage(), responseString)); + } + + packageHandler.sendNextPackage(); + } + + private String parseResponse(HttpResponse response) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.getEntity().writeTo(out); + out.close(); + String responseString = out.toString().trim(); + return responseString; + } + catch (Exception e) { + Logger.error(String.format("Failed to parse response (%s)", e)); + return "Failed to parse response"; + } + } + + private void closePackage(ActivityPackage activityPackage, String message, Throwable throwable) { + String failureMessage = activityPackage.getFailureMessage(); + if (throwable != null) { + Logger.error(String.format("%s. (%s: %s) Will retry later.", failureMessage, message, throwable)); + } else { + Logger.error(String.format("%s. (%s) Will retry later.", failureMessage, message)); + } + packageHandler.closeFirstPackage(); + } + + private void sendNextPackage(ActivityPackage activityPackage, String message, Throwable throwable) { + String failureMessage = activityPackage.getFailureMessage(); + if (throwable != null) { + Logger.error(String.format("%s (%s: %s)", failureMessage, message, throwable)); + } else { + Logger.error(String.format("%s (%s)", failureMessage, message)); + } + + packageHandler.sendNextPackage(); + } + + + private HttpUriRequest getRequest(ActivityPackage activityPackage) throws UnsupportedEncodingException { + String url = Util.BASE_URL + activityPackage.path; + HttpPost request = new HttpPost(url); + + String language = Locale.getDefault().getLanguage(); + request.addHeader("User-Agent", activityPackage.userAgent); + request.addHeader("Client-SDK", activityPackage.clientSdk); + request.addHeader("Accept-Language", language); + + List pairs = new ArrayList(); + for (Map.Entry entity : activityPackage.parameters.entrySet()) { + NameValuePair pair = new BasicNameValuePair(entity.getKey(), entity.getValue()); + pairs.add(pair); + } + + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs); + entity.setContentType(URLEncodedUtils.CONTENT_TYPE); + request.setEntity(entity); + + return request; + } +} diff --git a/AdjustIo/src/com/adeven/adjustio/RequestThread.java b/AdjustIo/src/com/adeven/adjustio/RequestThread.java deleted file mode 100644 index 4298acdf4..000000000 --- a/AdjustIo/src/com/adeven/adjustio/RequestThread.java +++ /dev/null @@ -1,118 +0,0 @@ -// -// RequestThread.java -// AdjustIo -// -// Created by Benjamin Weiss on 17.4.13 -// Copyright (c) 2012 adeven. All rights reserved. -// See the file MIT-LICENSE for copying permission. -// - -package com.adeven.adjustio; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.net.SocketException; - -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; - -/** - * Used to send tracking information. - * - * @author keyboardsurfer - * @since 17.4.13 - */ -public class RequestThread extends HandlerThread { - - private static final int MESSAGE_ARG_TRACK = 72400; - private Handler trackingHandler; - - public RequestThread() { - super(Logger.LOGTAG, MIN_PRIORITY); - setDaemon(true); - start(); - trackingHandler = new RequestHandler(getLooper(), this); - } - - void track(TrackingPackage information) { - Message message = Message.obtain(); - message.arg1 = MESSAGE_ARG_TRACK; - message.obj = information; - trackingHandler.sendMessage(message); - } - - private void trackInternal(TrackingPackage trackingInformation) { - HttpClient httpClient = Util.getHttpClient(trackingInformation.userAgent); - HttpPost request = Util.getPostRequest(trackingInformation.path); - - try { - request.setEntity(Util.getEntityEncodedParameters(trackingInformation.parameters)); - HttpResponse response = httpClient.execute(request); - Logger.info(getLogString(response, trackingInformation)); - } catch (SocketException e) { - Logger.error("This SDK requires the INTERNET permission. You might need to adjust your manifest. See the README for details."); - } catch (UnsupportedEncodingException e) { - Logger.error("Failed to encode parameters."); - } catch (IOException e) { - Logger.error("Unexpected IOException", e); - } - } - - private String getLogString(HttpResponse response, TrackingPackage trackingInformation) { - if (response == null) { - return trackingInformation.failureMessage + " (Request failed)"; - } else { - int statusCode = response.getStatusLine().getStatusCode(); - String responseString = parseResponse(response); - - if (statusCode == HttpStatus.SC_OK) { - return trackingInformation.successMessage; - } else { - return trackingInformation.failureMessage + " (" + responseString + ")"; - } - } - } - - private String parseResponse(HttpResponse response) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - response.getEntity().writeTo(out); - out.close(); - String responseString = out.toString().trim(); - return responseString; - } catch (Exception e) { - Logger.error("error parsing response", e); - return "Failed parsing response"; - } - } - - private static final class RequestHandler extends Handler { - private final WeakReference requestThreadReference; - - public RequestHandler(Looper looper, RequestThread requestThread) { - super(looper); - this.requestThreadReference = new WeakReference(requestThread); - } - - @Override - public void handleMessage(Message message) { - super.handleMessage(message); - RequestThread requestThread = requestThreadReference.get(); - if (requestThread == null) { - return; - } - if (message.arg1 == MESSAGE_ARG_TRACK) { - requestThread.trackInternal((TrackingPackage) message.obj); - } - } - } -} diff --git a/AdjustIo/src/com/adeven/adjustio/TrackingPackage.java b/AdjustIo/src/com/adeven/adjustio/TrackingPackage.java deleted file mode 100644 index bb7849583..000000000 --- a/AdjustIo/src/com/adeven/adjustio/TrackingPackage.java +++ /dev/null @@ -1,91 +0,0 @@ -// -// TrackingPackage.java -// AdjustIo -// -// Created by Benjamin Weiss on 17.4.13 -// Copyright (c) 2012 adeven. All rights reserved. -// See the file MIT-LICENSE for copying permission. -// - -package com.adeven.adjustio; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.http.NameValuePair; -import org.apache.http.message.BasicNameValuePair; - -/** - * Holds information of one tracking package. - * - * @author keyboardsurfer - * @since 17.4.13 - */ -public class TrackingPackage { - final String path; - final String successMessage; - final String failureMessage; - final String userAgent; - final List parameters; - - public TrackingPackage(String path, String successMessage, String failureMessage, String userAgent, List parameters) { - this.path = path; - this.successMessage = successMessage; - this.failureMessage = failureMessage; - this.userAgent = userAgent; - this.parameters = parameters; - } - - /** - * A builder to enable chained building of a RequestThread. - */ - static class Builder { - private String path; - private String successMessage; - private String failureMessage; - private String userAgent; - private List parameters; - - Builder() { - parameters = new ArrayList(); - } - - Builder setPath(String path) { - this.path = path; - return this; - } - - Builder setUserAgent(String userAgent) { - this.userAgent = userAgent; - Logger.verbose(path, "userAgent", userAgent); - return this; - } - - Builder setSuccessMessage(String successMessage) { - this.successMessage = successMessage; - Logger.verbose(path, "successMessage", successMessage); - return this; - } - - Builder setFailureMessage(String failureMessage) { - this.failureMessage = failureMessage; - Logger.verbose(path, "failureMessage", failureMessage); - return this; - } - - Builder addTrackingParameter(String key, String value) { - if (value == null || value == "" ) { - return this; - } - - parameters.add(new BasicNameValuePair(key, value)); - Logger.verbose(path, key, value); - return this; - } - - TrackingPackage build() { - TrackingPackage trackingPackage = new TrackingPackage(path, successMessage, failureMessage, userAgent, parameters); - return trackingPackage; - } - } -} diff --git a/AdjustIo/src/com/adeven/adjustio/Util.java b/AdjustIo/src/com/adeven/adjustio/Util.java index 9834cf8ff..1a6db0051 100644 --- a/AdjustIo/src/com/adeven/adjustio/Util.java +++ b/AdjustIo/src/com/adeven/adjustio/Util.java @@ -2,7 +2,7 @@ // Util.java // AdjustIo // -// Created by Christian Wellenbrock on 11.10.12. +// Created by Christian Wellenbrock on 2012-10-11. // Copyright (c) 2012 adeven. All rights reserved. // See the file MIT-LICENSE for copying permission. // @@ -12,21 +12,9 @@ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.math.BigInteger; import java.security.MessageDigest; -import java.util.List; import java.util.Locale; -import java.util.Map; - -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpParams; -import org.json.JSONObject; import android.content.ContentResolver; import android.content.Context; @@ -41,81 +29,18 @@ import android.os.Build; import android.provider.Settings.Secure; import android.text.TextUtils; -import android.util.Base64; import android.util.DisplayMetrics; + /** * Collects utility functions used by AdjustIo. - * - * @author wellle - * @since 11.10.12 */ public class Util { - private static final String BASEURL = "https://app.adjust.io"; - private static final String CLIENTSDK = "android1.6"; + protected static final String BASE_URL = "https://app.adjust.io"; + protected static final String CLIENT_SDK = "android2.0"; private static final String UNKNOWN = "unknown"; - public static boolean checkPermissions(Context context) { - boolean result = true; - - if (!checkPermission(context, android.Manifest.permission.INTERNET)) { - Logger.error( - "This SDK requires the INTERNET permission. " + - "See the README for details." - ); - result = false; - } - if (!checkPermission(context, android.Manifest.permission.ACCESS_WIFI_STATE)) { - Logger.warn( - "You can improve your tracking results by adding the " + - "ACCESS_WIFI_STATE permission. See the README for details." - ); - } - - return result; - } - - private static boolean checkPermission(Context context, String permission) { - int result = context.checkCallingOrSelfPermission(permission); - boolean granted = (result == PackageManager.PERMISSION_GRANTED); - return granted; - } - - public static String getBase64EncodedParameters(Map parameters) { - if (parameters == null) { - return null; - } - - JSONObject jsonObject = new JSONObject(parameters); - byte[] bytes = jsonObject.toString().getBytes(); - String encoded = Base64.encodeToString(bytes, Base64.NO_WRAP); - return encoded; - } - - public static StringEntity getEntityEncodedParameters(List parameters) throws UnsupportedEncodingException { - StringEntity entity = new UrlEncodedFormEntity(parameters); - return entity; - } - - public static HttpClient getHttpClient(String userAgent) { - HttpClient httpClient = new DefaultHttpClient(); - HttpParams params = httpClient.getParams(); - params.setParameter(CoreProtocolPNames.USER_AGENT, userAgent); - return httpClient; - } - - public static HttpPost getPostRequest(String path) { - String url = BASEURL + path; - HttpPost request = new HttpPost(url); - - String language = Locale.getDefault().getLanguage(); - request.addHeader("Accept-Language", language); - request.addHeader("Client-SDK", CLIENTSDK); - - return request; - } - protected static String getUserAgent(Context context) { Resources resources = context.getResources(); DisplayMetrics displayMetrics = resources.getDisplayMetrics(); @@ -317,7 +242,7 @@ private static String sanitizeString(String string, String defaultString) { return result; } - public static String loadAddress(String interfaceName) { + protected static String loadAddress(String interfaceName) { try { String filePath = "/sys/class/net/" + interfaceName + "/address"; StringBuffer fileData = new StringBuffer(1000); @@ -339,11 +264,11 @@ public static String loadAddress(String interfaceName) { } } - public static String getAndroidId(Context context) { + protected static String getAndroidId(Context context) { return Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); } - public static String getAttributionId(Context context) { + protected static String getAttributionId(Context context) { try { ContentResolver contentResolver = context.getContentResolver(); Uri uri = Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); @@ -367,36 +292,29 @@ public static String getAttributionId(Context context) { } } - public static String sha1(String text) { + protected static String sha1(String text) { + return hash(text, "SHA-1"); + } + + protected static String md5(String text) { + return hash(text, "MD5"); + } + + private static String hash(String text, String method) { try { - MessageDigest mesd = MessageDigest.getInstance("SHA-1"); - byte[] bytes = text.getBytes("iso-8859-1"); + byte[] bytes = text.getBytes("UTF-8"); + MessageDigest mesd = MessageDigest.getInstance(method); mesd.update(bytes, 0, bytes.length); - byte[] sha2hash = mesd.digest(); - return convertToHex(sha2hash); + byte[] hash = mesd.digest(); + return convertToHex(hash); } catch (Exception e) { return ""; } } private static String convertToHex(byte[] bytes) { - StringBuffer buffer = new StringBuffer(); - for (int i = 0; i < bytes.length; i++) { - int halfbyte = (bytes[i] >>> 4) & 0x0F; - int two_halfs = 0; - - do { - if ((0 <= halfbyte) && (halfbyte <= 9)) { - buffer.append((char) ('0' + halfbyte)); - } else { - buffer.append((char) ('a' + (halfbyte - 10))); - } - - halfbyte = bytes[i] & 0x0F; - } while (two_halfs++ < 1); - } - - String hex = buffer.toString(); - return hex; + BigInteger bigInt = new BigInteger(1, bytes); + String formatString = "%0" + (bytes.length << 1) + "x"; + return String.format(formatString, bigInt); } } diff --git a/README.md b/README.md index 9691994ab..b8a0127d4 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,210 @@ ## Summary -This is the Android SDK of AdjustIo. You ca read more about it at [adjust.io][]. +This is the Android SDK of AdjustIo. You ca read more about AdjustIo at +[adjust.io]. ## Basic Installation -These are the minimal steps required to integrate the AdjustIo SDK into your Android project. We are going to assume that you use Eclipse for your Android development. +These are the minimal steps required to integrate the AdjustIo SDK into your +Android project. We are going to assume that you use Eclipse for your Android +development. ### 1. Get the SDK -Download the latest version from our [tags page][tags]. Extract the archive in a folder of your liking. -### 2. Add it to your project +Download the latest version from our [releases page][releases]. Extract the +archive in a folder of your choice. + +### 2. Create the AdjustIo project + In the Eclipse menu select `File|New|Project...`. ![][project] -From the Wizard expand the `Android` group and select `Android Project from Existing Code` and click `Next`. +From the Wizard expand the `Android` group and select `Android Project from +Existing Code` and click `Next`. ![][android] -On the top of the next screen click the `Browse...` button and locate the folder you extracted in step 1. Select the AdjustIo subfolder and click `Open`. In the `Projects:` group make sure the AdjustIo project is selected. Also tick the option `Copy projects into workspace` and click `Finish`. +On the top of the next screen click the `Browse...` button and locate the +folder you extracted in step 1. Select the AdjustIo subfolder and click `Open`. +In the `Projects:` group make sure the AdjustIo project is selected. Also tick +the option `Copy projects into workspace` and click `Finish`. ![][import] -### 3. Integrate AdjustIo into your app -In the Package Explorer right click on your Android project and select `Properties`. +### 3. Add the AdjustIo library to your project + +In the Package Explorer right click on your Android project and select +`Properties`. ![][properties] -In the left pane select `Android`. In the bottom right group `Library` click the `Add...` button. From the list select the AdjustIo library project and click `OK`. Save your changed project properties by clicking `OK` again. +In the left pane select `Android`. In the bottom right group `Library` click +the `Add...` button. From the list select the AdjustIo library project and +click `OK`. Save your changed project properties by clicking `OK` again. ![][library] -In the Package Explorer open the `AndroidManifest.xml` of your Android project. Add the `uses-permission` tags for `INTERNET` and `ACCESS_WIFI_STATE` if they aren't present already. +### 4. Add permissions - - +In the Package Explorer open the `AndroidManifest.xml` of your Android project. +Add the `uses-permission` tags for `INTERNET` and `ACCESS_WIFI_STATE` if they +aren't present already. -![][permissions] +```java + + +``` -In the Package Explorer open the launch activity of your Android App. Add the `import` statement to the top of the source file. In the `onCreate` method of your activity call the method `appDidLaunch`. This tells AdjustIo about the launch of your Application. +![][permissions] - import com.adeven.adjustio.AdjustIo; +### 5. Integrate AdjustIo into your app + +To provide proper session tracking it is required to call certain AdjustIo +methods every time any Activity resumes or pauses. Otherwise the SDK might miss +a session start or session end. In order to do so you should follow these steps +for **each** Activity of your app: + +- Open the source file of your Activity. +- Add the `import` statement at the top of the file. +- In your Activity's `onResume` method call `AdjustIo.onResume`. Create the + method if needed. +- Replace `{YourAppToken}` with your App Token. You can find in your + [dashboard]. +- In your Activity's `orPause` method call `AdjustIo.onPause`. Create the + method if needed. + +After these steps your activity should look like this: + +```java +import com.adeven.adjustio.AdjustIo; +// ... +public class YourActivity extends Activity { + protected void onResume() { + super.onResume(); + AdjustIo.onResume("{YourAppToken}", this); + } + protected void onPause() { + super.onPause(); + AdjustIo.onPause(); + } // ... - AdjustIo.appDidLaunch(getApplication()); +} +``` ![][activity] -### 4. Build your app -Build and run your Android app. In your LogCat viewer you can set the filter `tag:AdjustIo` to hide all other logs. After your app has launched you should see the following AdjustIo log: `Tracked session start.` +Repeat these steps for **every** Activity of your app. Don't forget these steps +when you create new Activities in the future. Depending on your coding style +you might want to implement this in a common superclass of all your Activities. + +### 6. Build your app + +Build and run your Android app. In your LogCat viewer you can set the filter +`tag:AdjustIo` to hide all other logs. After your app has launched you should +see the following AdjustIo log: `Tracked session start` ![][log] +### 7. Adjust Logging + +You can increase or decrease the amount of logs you see by calling +`setLogLevel` with one of the following parameters. Make sure to import +`android.util.Log`. + +```java +AdjustIo.setLogLevel(Log.VERBOSE); // enable all logging +AdjustIo.setLogLevel(Log.DEBUG); // enable more logging +AdjustIo.setLogLevel(Log.INFO); // the default +AdjustIo.setLogLevel(Log.WARN); // disable info logging +AdjustIo.setLogLevel(Log.ERROR); // disable warnings as well +AdjustIo.setLogLevel(Log.ASSERT); // disable errors as well +``` + ## Additional Features -Once you have integrated the AdjustIo SDK into you project, you can take advantage of the following features wherever you see fit. +Once you have integrated the AdjustIo SDK into your project, you can take +advantage of the following features. ### Add tracking of custom events. -You can tell AdjustIo about every event you consider to be of your interest. Suppose you want to track every tap on a button. Currently you would have to ask us for an eventId and we would give you one, like `abc123`. In your button's onClick method you could then add the following code to track the click: - AdjustIo.trackEvent("abc123"); +You can tell AdjustIo about every event you want. Suppose you want to track +every tap on a button. You would have to create a new Event Token in your +[dashboard]. Let's say that Event Token is `abc123`. In your button's `onClick` +method you could then add the following line to track the click: + +```java +AdjustIo.trackEvent("abc123"); +``` + +You can also register a callback URL for that event in your [dashboard] and we +will send a GET request to that URL whenever the event gets tracked. In that +case you can also put some key-value-pairs in a dictionary and pass it to the +`trackEvent` method. We will then append these named parameters to your +callback URL. + +For example, suppose you have registered the URL +`http://www.adeven.com/callback` for your event with Event Token `abc123` and +execute the following lines: + +```java +Map parameters = new HashMap(); +parameters.put("key", "value"); +parameters.put("foo", "bar"); +AdjustIo.trackEvent("abc123", parameters); +``` -You can also register a callback URL for that event and we will send a request to that URL whenever the event happens. Additianally you can put some key-value-pairs in a Map and pass it to the trackEvent method. In that case we will forward these named parameters to your callback URL. Suppose you registered the URL `http://www.adeven.com/callback` for your event and execute the following lines: +In that case we would track the event and send a request to: - Map parameters = new HashMap(); - parameters.put("key", "value"); - parameters.put("foo", "bar"); - AdjustIo.trackEvent("abc123", parameters); + http://www.adeven.com/callback?key=value&foo=bar -In that case we would track the event and send a request to `http://www.adeven.com/callback?key=value&foo=bar`. In any case you need to import AdjustIo in any source file that makes use of the SDK. Please note that we don't store your custom parameters. If you haven't registered a callback URL for an event, there is no point in sending us parameters. +It should be mentioned that we support a variety of placeholders like +`{android_id}` that can be used as parameter values. In the resulting callback +this placeholder would be replaced with the AndroidID of the current device. +Also note that we don't store any of your custom parameters, but only append +them to your callbacks. If you haven't registered a callback for an event, +these parameters won't even be read. ### Add tracking of revenue -If your users can generate revenue by clicking on advertisements you can track those revenues. If the click is worth one Cent, you could make the following call to track that revenue: - AdjustIo.trackRevenue(1.0f); +If your users can generate revenue by clicking on advertisements or making +purchases you can track those revenues. If, for example, a click is worth one +cent, you could make the following call to track that revenue: -The parameter is supposed to be in Cents and will get rounded to one decimal point. If you want to differentiate between different kinds of revenue you can get different eventIds for each kind. Again, you need to ask us for eventIds that you can then use. In that case you would make a call like this: +```java +AdjustIo.trackRevenue(1.0); +``` - AdjustIo.trackRevenue(1.0f, "abc123"); +The parameter is supposed to be in cents and will get rounded to one decimal +point. If you want to differentiate between different kinds of revenue you can +get different Event Tokens for each kind. Again, you need to create those Event +Tokens in your [dashboard]. In that case you would make a call like this: -You can also register a callback URL again and provide a map of named parameters, just like it worked with normal events. +```java +AdjustIo.trackRevenue(1.0, "abc123"); +``` - Map parameters = new HashMap(); - parameters.put("key", "value"); - parameters.put("foo", "bar"); - AdjustIo.trackRevenue(1.0f, "abc123", parameters); +Again, you can register a callback and provide a dictionary of named +parameters, just like it worked with normal events. -In any case, don't forget to import AdjustIo. Again, there is no point in sending parameters if you haven't registered a callback URL for that revenue event. +```java +Map parameters = new HashMap(); +parameters.put("key", "value"); +parameters.put("foo", "bar"); +AdjustIo.trackRevenue(1.0, "abc123", parameters); +``` -[adjust.io]: http://www.adjust.io -[tags]: https://github.com/adeven/adjust_android_sdk/tags +[adjust.io]: http://adjust.io +[dashboard]: http://adjust.io +[releases]: https://github.com/adeven/adjust_android_sdk/releases [project]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/project.png [android]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/android.png [import]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/import.png [properties]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/properties.png [library]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/library.png [permissions]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/permissions.png -[activity]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/activity.png -[log]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/log.png +[activity]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/activity2.png +[log]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/log2.png ## License @@ -106,21 +214,20 @@ The adjust-sdk is licensed under the MIT License. Copyright (c) 2012 adeven GmbH, http://www.adeven.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. +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/VERSION b/VERSION index 810ee4e91..cd5ac039d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6 +2.0 diff --git a/doc/migrate.md b/doc/migrate.md new file mode 100644 index 000000000..4f6ae55a9 --- /dev/null +++ b/doc/migrate.md @@ -0,0 +1,59 @@ +## Migrate to AdjustIo SDK for Android v2.0 + +1. Delete the old `AdjustIo` project from your `Package Explorer`. Download + version v2.0 and create a new `Android Project from Existing Code` as + described in the [README]. + + ![][import] + +2. We no longer use the `AdjustIo.appDidLaunch()` method for initialization. + Delete the call in your launch activity's `onCreate` method. + +3. Instead, to provide proper session tracking, it is required to call certain + new AdjustIo methods every time any Activity resumes or pauses. Otherwise + the SDK might miss a session start or session end. In order to do so you + should follow these steps for **each** Activity of your app: + + - Open the source file of your Activity. + - Add the `import` statement at the top of the file. + - In your Activity's `onResume` method call `AdjustIo.onResume`. Create the + method if needed. + - Replace `{YourAppToken}` with your App Token. You can find in your + [dashboard]. + - In your Activity's `orPause` method call `AdjustIo.onPause`. Create the + method if needed. + + After these steps your activity should look like this: + + ```java + import com.adeven.adjustio.AdjustIo; + // ... + public class YourActivity extends Activity { + protected void onResume() { + super.onResume(); + AdjustIo.onResume("{YourAppToken}", this); + } + protected void onPause() { + super.onPause(); + AdjustIo.onPause(); + } + // ... + } + ``` + + ![][activity] + + Repeat these steps for **every** Activity of your app. Don't forget these + steps when you create new Activities in the future. Depending on your + coding style you might want to implement this in a common superclass of all + your Activities. + +4. The `amount` parameter of the `trackRevenue` methods is now of type + `double`, so you can drop the `f` suffixes in number literals (`12.3f` + becomes `12.3`). + +[README]: ../README.md +[import]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/import.png +[activity]: https://raw.github.com/adeven/adjust_sdk/master/Resources/android/activity2.png +[dashboard]: http://adjust.io +