Bug 1143888 - Integrate the Adjust install tracking SDK. r=mfinkle, a=sledru
authorMark Finkle <mfinkle@mozilla.com>
Wed, 08 Apr 2015 22:49:16 -0700
changeset 265796 e831b9c65796cbc2e98b043b1706d41cf89097c4
parent 265795 4ce2712ce3f3513d39f7a5c28fe36e876575961e
child 265797 838556f52b8da634898c7d6ca7a42212dd299d84
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, sledru
bugs1143888
milestone39.0a2
Bug 1143888 - Integrate the Adjust install tracking SDK. r=mfinkle, a=sledru
configure.in
mobile/android/base/AppConstants.java.in
mobile/android/base/GeckoApplication.java
mobile/android/base/Makefile.in
mobile/android/base/adjust/AdjustHelper.java
mobile/android/base/adjust/AdjustHelperInterface.java
mobile/android/base/adjust/StubAdjustHelper.java
mobile/android/base/adjust/adjust.rst
mobile/android/base/distribution/ReferrerReceiver.java
mobile/android/base/moz.build
mobile/android/config/proguard/adjust-keeps.cfg
mobile/android/config/proguard/proguard.cfg
mobile/android/confvars.sh
mobile/android/gradle/base/build.gradle
mobile/android/gradle/preprocessed_code/build.gradle
mobile/android/gradle/settings.gradle
mobile/android/gradle/thirdparty/build.gradle
mobile/android/gradle/thirdparty_adjust_sdk/AndroidManifest.xml
mobile/android/gradle/thirdparty_adjust_sdk/build.gradle
mobile/android/mach_commands.py
mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
mobile/android/thirdparty/com/adjust/sdk/Adjust.java
mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
mobile/android/thirdparty/com/adjust/sdk/Constants.java
mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
mobile/android/thirdparty/com/adjust/sdk/ILogger.java
mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
mobile/android/thirdparty/com/adjust/sdk/Logger.java
mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
mobile/android/thirdparty/com/adjust/sdk/Reflection.java
mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
mobile/android/thirdparty/com/adjust/sdk/Util.java
mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
--- a/configure.in
+++ b/configure.in
@@ -3951,16 +3951,17 @@ MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
 MOZ_LOCALE_SWITCHER=
 MOZ_ANDROID_READING_LIST_SERVICE=
 MOZ_ANDROID_SEARCH_ACTIVITY=
 MOZ_ANDROID_DOWNLOADS_INTEGRATION=
 MOZ_ANDROID_MLS_STUMBLER=
 MOZ_ANDROID_SHARE_OVERLAY=
+MOZ_INSTALL_TRACKING=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_PAY=
 MOZ_AUDIO_CHANNEL_MANAGER=
 NSS_NO_LIBPKIX=
 MOZ_CONTENT_SANDBOX=
 MOZ_GMP_SANDBOX=
 MOZ_SANDBOX=1
@@ -4089,16 +4090,23 @@ MOZ_ARG_WITH_STRING(bing-api-keyfile,
   MOZ_BING_API_KEY=`cat $withval | cut -f 2 -d " "`])
 if test -z "$MOZ_BING_API_CLIENTID"; then
     MOZ_BING_API_CLIENTID=no-bing-api-clientid
     MOZ_BING_API_KEY=no-bing-api-key
 fi
 AC_SUBST(MOZ_BING_API_CLIENTID)
 AC_SUBST(MOZ_BING_API_KEY)
 
+# Allow specifying an Adjust SDK key file that contains the app token used for
+# Adjust SDK requests.
+MOZ_ARG_WITH_STRING(adjust-sdk-keyfile,
+[  --with-adjust-sdk-keyfile=file   Use the app token contained in the given keyfile for Adjust SDK requests],
+  MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN=`cat $withval`)
+AC_SUBST(MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN)
+
 # Whether to include optional-but-large font files in the final APK.
 # We want this in mobile/android/confvars.sh, so it goes early.
 MOZ_ARG_DISABLE_BOOL(android-include-fonts,
 [  --disable-android-include-fonts
                           Disable the inclusion of fonts into the final APK],
     MOZ_ANDROID_EXCLUDE_FONTS=1)
 
 if test -n "$MOZ_ANDROID_EXCLUDE_FONTS"; then
@@ -5015,16 +5023,23 @@ fi
 dnl = Include Tab Queue on Android
 dnl = Temporary build flag to allow development in Nightly
 dnl ========================================================
 if test -n "$MOZ_ANDROID_TAB_QUEUE"; then
     AC_DEFINE(MOZ_ANDROID_TAB_QUEUE)
 fi
 
 dnl ========================================================
+dnl = Include install tracking on Android
+dnl ========================================================
+if test -n "$MOZ_INSTALL_TRACKING"; then
+    AC_DEFINE(MOZ_INSTALL_TRACKING)
+fi
+
+dnl ========================================================
 dnl = Enable IPDL's "expensive" unit tests
 dnl ========================================================
 MOZ_IPDL_TESTS=
 
 MOZ_ARG_ENABLE_BOOL(ipdl-tests,
 [  --enable-ipdl-tests     Enable expensive IPDL tests],
     MOZ_IPDL_TESTS=1,
     MOZ_IPDL_TESTS=)
@@ -8514,16 +8529,17 @@ AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(MOZ_ANDROID_GECKOLIBS_AAR)
 AC_SUBST(MOZ_ANDROID_READING_LIST_SERVICE)
 AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
 AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
 AC_SUBST(MOZ_ANDROID_TAB_QUEUE)
 AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
 AC_SUBST(MOZ_ANDROID_DOWNLOADS_INTEGRATION)
+AC_SUBST(MOZ_INSTALL_TRACKING)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
 AC_SUBST(MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS)
 AC_SUBST(MOZ_COMPONENT_NSPR_LIBS)
 
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -3,16 +3,23 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.os.Build;
 
+import org.mozilla.gecko.adjust.AdjustHelperInterface;
+//#ifdef MOZ_INSTALL_TRACKING
+import org.mozilla.gecko.adjust.AdjustHelper;
+//#else
+import org.mozilla.gecko.adjust.StubAdjustHelper;
+//#endif
+
 /**
  * A collection of constants that pertain to the build and runtime state of the
  * application. Typically these are sourced from build-time definitions (see
  * Makefile.in). This is a Java-side substitute for nsIXULAppInfo, amongst
  * other things.
  *
  * See also SysInfo.java, which includes some of the values available from
  * nsSystemInfo inside Gecko.
@@ -292,9 +299,30 @@ public class AppConstants {
 //#ifdef MOZ_LINKER_EXTRACT
     true;
 //#else
     false;
 //#endif
 
     public static final boolean MOZ_DRAGGABLE_URLBAR = false;
 
+    public static final boolean MOZ_INSTALL_TRACKING =
+//#ifdef MOZ_INSTALL_TRACKING
+    true;
+//#else
+    false;
+//#endif
+
+    public static final String MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN =
+//#ifdef MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN
+    "@MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN@";
+//#else
+    null;
+//#endif
+
+    public static AdjustHelperInterface getAdjustHelper() {
+//#ifdef MOZ_INSTALL_TRACKING
+        return new AdjustHelper();
+//#else
+        return new StubAdjustHelper();
+//#endif
+    }
 }
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -1,14 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
@@ -147,16 +148,20 @@ public class GeckoApplication extends Ap
                 // Note that we don't use the profile directory -- we
                 // send operations to the ContentProvider, which does
                 // its own thing.
                 return new LocalBrowserDB(profileName);
             }
         });
 
         super.onCreate();
+
+        if (AppConstants.MOZ_INSTALL_TRACKING) {
+            AppConstants.getAdjustHelper().onCreate(this, AppConstants.MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN);
+        }
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
     public LightweightTheme getLightweightTheme() {
         return mLightweightTheme;
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -118,16 +118,20 @@ ALL_JARS += search-activity.jar
 endif
 
 ifdef MOZ_ANDROID_MLS_STUMBLER
 extra_packages += org.mozilla.mozstumbler
 ALL_JARS += ../stumbler/stumbler.jar
 generated/org/mozilla/mozstumbler/R.java: .aapt.deps ;
 endif
 
+ifdef MOZ_INSTALL_TRACKING
+ALL_JARS += gecko-thirdparty-adjust_sdk.jar
+endif
+
 # The list of jars in Java classpath notation (colon-separated).
 all_jars_classpath := $(subst $(NULL) ,:,$(strip $(ALL_JARS)))
 
 include $(topsrcdir)/config/config.mk
 
 library_jars := \
     $(ANDROID_SDK)/android.jar \
     $(NULL)
@@ -251,16 +255,35 @@ PP_TARGETS += manifest
 # need to write AppConstants.java.in to
 # generated/preprocessed/org/mozilla/gecko.
 preprocessed := $(addsuffix .in,$(subst generated/preprocessed/org/mozilla/gecko/,,$(filter generated/preprocessed/org/mozilla/gecko/%,$(constants_PP_JAVAFILES))))
 
 preprocessed_PATH := generated/preprocessed/org/mozilla/gecko
 preprocessed_KEEP_PATH := 1
 preprocessed_FLAGS := --marker='//\\\#'
 
+ifdef MOZ_INSTALL_TRACKING
+ifdef MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN
+# The value of MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN is sensitive: we don't
+# want to expose it in build logs.  Following the model set by
+# toolkit/components/urlformatter/Makefile.in, we expose it to AppConstants.java
+# quietly here.  Be aware that the included line's marker needs to agree with
+# --marker above.
+
+adjust_sdk_app_token:
+	@echo '//#define MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN $(MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN)' > $@
+
+export:: adjust_sdk_app_token
+
+preprocessed_FLAGS += -I adjust_sdk_app_token
+endif # MOZ_INSTALL_TRACKING
+endif # MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN
+
+GARBAGE += adjust_sdk_app_token
+
 PP_TARGETS += preprocessed
 
 include $(topsrcdir)/config/rules.mk
 
 not_android_res_files := \
   *.mkdir.done* \
   *.DS_Store* \
   *\#* \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/adjust/AdjustHelper.java
@@ -0,0 +1,35 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.adjust;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.adjust.sdk.Adjust;
+import com.adjust.sdk.AdjustConfig;
+import com.adjust.sdk.AdjustReferrerReceiver;
+import com.adjust.sdk.LogLevel;
+
+public class AdjustHelper implements AdjustHelperInterface {
+    public void onCreate(final Context context, final String maybeAppToken) {
+        final String environment;
+        final String appToken;
+        if (maybeAppToken != null) {
+            environment = AdjustConfig.ENVIRONMENT_PRODUCTION;
+            appToken = maybeAppToken;
+        } else {
+            environment = AdjustConfig.ENVIRONMENT_SANDBOX;
+            appToken = "ABCDEFGHIJKL";
+        }
+        AdjustConfig config = new AdjustConfig(context, appToken, environment);
+        config.setLogLevel(LogLevel.VERBOSE);
+        Adjust.onCreate(config);
+    }
+
+    public void onReceive(final Context context, final Intent intent) {
+        new AdjustReferrerReceiver().onReceive(context, intent);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/adjust/AdjustHelperInterface.java
@@ -0,0 +1,18 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.adjust;
+
+import android.content.Context;
+import android.content.Intent;
+
+public interface AdjustHelperInterface {
+    /**
+     * Register the Application with the Adjust SDK.
+     * @param appToken the (secret!) Adjust SDK per-application token to register with; may be null.
+     */
+    void onCreate(final Context context, final String appToken);
+    void onReceive(final Context context, final Intent intent);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/adjust/StubAdjustHelper.java
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.adjust;
+
+import android.content.Context;
+import android.content.Intent;
+
+public class StubAdjustHelper implements AdjustHelperInterface {
+    public void onCreate(final Context context, final String appToken) {
+        // Do nothing.
+    }
+
+    public void onReceive(final Context context, final Intent intent) {
+        // Do nothing.
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/adjust/adjust.rst
@@ -0,0 +1,19 @@
+Adjust SDK integration
+======================
+
+The *Adjust install tracking SDK* is a pure-Java library that is conditionally
+compiled into Fennec.  It's not trivial to integrate such conditional feature
+libraries into Fennec without pre-processing.  To minimize such pre-processing,
+we define a trivial ``AdjustHelperInterface`` and define two implementations:
+the real ``AdjustHelper``, which requires the Adjust SDK, and a no-op
+``StubAdjustHelper``, which has no additional requirements.  We use the existing
+pre-processed ``AppConstants.java.in`` to switch, at build-time, between the two
+implementations.
+
+An alternative approach would be to build three jars -- one interface jar and
+two implementation jars -- and include one of the implementation jars at
+build-time.  The implementation jars could either define a common symbol, or the
+appropriate symbol could be determined at build-time.  That's a rather heavier
+approach than the one chosen.  If the helper class were to grow to multiple
+classes, with a non-trivial exposed API, this approach could be better.  It
+would also be easier to integrate into the parallel Gradle build system.
--- a/mobile/android/base/distribution/ReferrerReceiver.java
+++ b/mobile/android/base/distribution/ReferrerReceiver.java
@@ -1,21 +1,23 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.distribution;
 
-import org.json.JSONException;
-import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class ReferrerReceiver extends BroadcastReceiver {
@@ -44,16 +46,24 @@ public class ReferrerReceiver extends Br
             // This should never happen.
             return;
         }
 
         // Track the referrer object for distribution handling.
         ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer"));
 
         if (!TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
+            if (AppConstants.MOZ_INSTALL_TRACKING) {
+                // Allow the Adjust handler to process the intent.
+                try {
+                    AppConstants.getAdjustHelper().onReceive(context, intent);
+                } catch (Exception e) {
+                    Log.e(LOGTAG, "Got exception in Adjust's onReceive; ignoring referrer intent.", e);
+                }
+            }
             return;
         }
 
         if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) {
             Distribution.onReceivedReferrer(context, referrer);
         } else {
             Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
             // If this is a Mozilla campaign, pass the campaign along to Gecko.
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -8,22 +8,35 @@ DIRS += ['locales']
 SPHINX_TREES['fennec'] = 'docs'
 
 include('android-services.mozbuild')
 
 thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
 
 constants_jar = add_java_jar('constants')
 constants_jar.sources = [
+    'adjust/AdjustHelperInterface.java',
     'SysInfo.java',
 ]
 constants_jar.generated_sources = [
     'preprocessed/org/mozilla/gecko/AppConstants.java',
 ]
 
+if CONFIG['MOZ_INSTALL_TRACKING']:
+    constants_jar.sources += [
+        'adjust/AdjustHelper.java',
+    ]
+    constants_jar.extra_jars = [
+        'gecko-thirdparty-adjust_sdk.jar',
+    ]
+else:
+    constants_jar.sources += [
+        'adjust/StubAdjustHelper.java',
+    ]
+
 resjar = add_java_jar('gecko-R')
 resjar.sources = []
 resjar.generated_sources += [
     'org/mozilla/gecko/R.java',
 ]
 
 if CONFIG['MOZ_NATIVE_DEVICES']:
     resjar.generated_sources += ['com/google/android/gms/R.java']
@@ -670,31 +683,70 @@ gtjar.sources += [ thirdparty_source_dir
 #gtjar.javac_flags += ['-Xlint:all']
 
 if not CONFIG['MOZILLA_OFFICIAL']:
     gtjar.sources += [ thirdparty_source_dir + f for f in [
         'org/lucasr/dspec/DesignSpec.java',
         'org/lucasr/dspec/RawResource.java'
     ] ]
 
+if CONFIG['MOZ_INSTALL_TRACKING']:
+    adjustjar = add_java_jar('gecko-thirdparty-adjust_sdk')
+    adjustjar.sources += [ thirdparty_source_dir + f for f in [
+        'com/adjust/sdk/ActivityHandler.java',
+        'com/adjust/sdk/ActivityKind.java',
+        'com/adjust/sdk/ActivityPackage.java',
+        'com/adjust/sdk/ActivityState.java',
+        'com/adjust/sdk/Adjust.java',
+        'com/adjust/sdk/AdjustAttribution.java',
+        'com/adjust/sdk/AdjustConfig.java',
+        'com/adjust/sdk/AdjustEvent.java',
+        'com/adjust/sdk/AdjustFactory.java',
+        'com/adjust/sdk/AdjustInstance.java',
+        'com/adjust/sdk/AdjustReferrerReceiver.java',
+        'com/adjust/sdk/AttributionHandler.java',
+        'com/adjust/sdk/Constants.java',
+        'com/adjust/sdk/DeviceInfo.java',
+        'com/adjust/sdk/IActivityHandler.java',
+        'com/adjust/sdk/IAttributionHandler.java',
+        'com/adjust/sdk/ILogger.java',
+        'com/adjust/sdk/IPackageHandler.java',
+        'com/adjust/sdk/IRequestHandler.java',
+        'com/adjust/sdk/Logger.java',
+        'com/adjust/sdk/LogLevel.java',
+        'com/adjust/sdk/OnAttributionChangedListener.java',
+        'com/adjust/sdk/PackageBuilder.java',
+        'com/adjust/sdk/PackageHandler.java',
+        'com/adjust/sdk/plugin/AndroidIdUtil.java',
+        'com/adjust/sdk/plugin/MacAddressUtil.java',
+        'com/adjust/sdk/plugin/Plugin.java',
+        'com/adjust/sdk/Reflection.java',
+        'com/adjust/sdk/RequestHandler.java',
+        'com/adjust/sdk/UnitTestActivity.java',
+        'com/adjust/sdk/Util.java'
+    ] ]
+
 # Putting branding earlier allows branders to override default resources.
 ANDROID_RES_DIRS += [
     TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res',
     SRCDIR + '/resources',
     OBJDIR + '/res',
 ]
 
 ANDROID_GENERATED_RESFILES += [
     'res/raw/suggestedsites.json',
     'res/values/strings.xml',
 ]
 
+# We do not expose MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN here because that
+# would leak the value to build logs.  Instead we expose the token quietly where
+# appropriate in Makefile.in.
 for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
             'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
-            'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION',
+            'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING',
             'MOZ_ANDROID_TAB_QUEUE'):
     if CONFIG[var]:
         DEFINES[var] = 1
 
 for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
     if CONFIG[var]:
         DEFINES[var] = CONFIG[var]
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/config/proguard/adjust-keeps.cfg
@@ -0,0 +1,8 @@
+# Rules to make the Adjust install tracking library work.
+
+# Needed to keep some constants in the install tracking library.
+-keep class com.adjust.sdk.** { *; }
+
+# Needed to keep some constants required for the install tracking library.
+-keep class com.google.android.gms.common.** { *; }
+-keep class com.google.android.gms.ads.identifier.** { *; }
--- a/mobile/android/config/proguard/proguard.cfg
+++ b/mobile/android/config/proguard/proguard.cfg
@@ -213,8 +213,10 @@
 # Suppress warnings about missing descriptor classes.
 #-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
 
 -include "play-services-keeps.cfg"
 
 # Don't print spurious warnings from the support library.
 # See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
 -dontnote android.support.**
+
+-include "adjust-keeps.cfg"
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -62,16 +62,24 @@ MOZ_LOCALE_SWITCHER=1
 MOZ_DEVICES=1
 
 # Enable second screen using native Android libraries, provided we're
 # not resource constrained.
 if test -z "$MOZ_ANDROID_RESOURCE_CONSTRAINED"; then
   MOZ_NATIVE_DEVICES=1
 fi
 
+# Enable install tracking SDK if we have Google Play support; MOZ_NATIVE_DEVICES
+# is a proxy flag for that support.
+if test "$RELEASE_BUILD"; then
+if test "$MOZ_NATIVE_DEVICES"; then
+  MOZ_INSTALL_TRACKING=1
+fi
+fi
+
 # Mark as WebGL conformant
 MOZ_WEBGL_CONFORMANT=1
 
 # Enable the Search Activity.
 MOZ_ANDROID_SEARCH_ACTIVITY=1
 
 # Enable the share handler.
 MOZ_ANDROID_SHARE_OVERLAY=1
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -32,16 +32,18 @@ android {
     sourceSets {
         main {
             java {
                 exclude 'org/mozilla/gecko/tests/**'
                 exclude 'org/mozilla/gecko/resources/**'
                 if (!mozconfig.substs.MOZ_CRASHREPORTER) {
                     exclude 'org/mozilla/gecko/CrashReporter.java'
                 }
+                // Adjust helpers are included in the preprocessed_code project.
+                exclude 'org/mozilla/gecko/adjust/**'
             }
 
             res {
                 if (mozconfig.substs.MOZ_CRASHREPORTER) {
                     srcDir "src/crashreporter/res"
                 }
             }
         }
--- a/mobile/android/gradle/preprocessed_code/build.gradle
+++ b/mobile/android/gradle/preprocessed_code/build.gradle
@@ -16,13 +16,33 @@ android {
 
     dexOptions {
         incremental true
     }
 
     lintOptions {
         abortOnError false
     }
+
+    sourceSets {
+        main {
+            java {
+                srcDir "${topsrcdir}/mobile/android/base/adjust"
+                if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
+                    exclude 'StubAdjustHelper.java'
+                } else {
+                    exclude 'AdjustHelper.java'
+                }
+            }
+        }
+    }
 }
 
 android.libraryVariants.all { variant ->
     variant.checkManifest.dependsOn generateCodeAndResources
 }
+
+
+dependencies {
+    if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
+        compile project(':thirdparty_adjust_sdk')
+    }
+}
--- a/mobile/android/gradle/settings.gradle
+++ b/mobile/android/gradle/settings.gradle
@@ -35,16 +35,21 @@ def gradleRoot = new File("${json.topobj
 project(':app').projectDir = new File(gradleRoot, 'app')
 project(':base').projectDir = new File(gradleRoot, 'base')
 project(':branding').projectDir = new File(gradleRoot, 'branding')
 project(':omnijar').projectDir = new File(gradleRoot, 'omnijar')
 project(':preprocessed_code').projectDir = new File(gradleRoot, 'preprocessed_code')
 project(':preprocessed_resources').projectDir = new File(gradleRoot, 'preprocessed_resources')
 project(':thirdparty').projectDir = new File(gradleRoot, 'thirdparty')
 
+if (json.substs.MOZ_INSTALL_TRACKING) {
+    include ':thirdparty_adjust_sdk'
+    project(':thirdparty_adjust_sdk').projectDir = new File(gradleRoot, 'thirdparty_adjust_sdk')
+}
+
 // The Gradle instance is shared between settings.gradle and all the
 // other build.gradle files (see
 // http://forums.gradle.org/gradle/topics/define_extension_properties_from_settings_xml).
 // We use this ext property to pass the per-object-directory mozconfig
 // between scripts.  This lets us execute set-up code before we gradle
 // tries to configure the project even once, and as a side benefit
 // saves invoking |mach environment| multiple times.
 gradle.ext.mozconfig = json
--- a/mobile/android/gradle/thirdparty/build.gradle
+++ b/mobile/android/gradle/thirdparty/build.gradle
@@ -16,14 +16,23 @@ android {
 
     dexOptions {
         incremental true
     }
 
     lintOptions {
         abortOnError false
     }
+
+    sourceSets {
+        main {
+            java {
+                // Thirdparty Adjust SDK code is included in the thirdparty_adjust_sdk project.
+                exclude 'com/adjust/**'
+            }
+        }
+    }
 }
 
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.android.support:support-v4:21.+'
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/thirdparty_adjust_sdk/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.adjust.sdk">
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/thirdparty_adjust_sdk/build.gradle
@@ -0,0 +1,37 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 21
+    buildToolsVersion "21.1.1"
+
+    defaultConfig {
+        targetSdkVersion 21
+        minSdkVersion 9
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    dexOptions {
+        incremental true
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    sourceSets {
+        main {
+            java {
+                srcDir "${topsrcdir}/mobile/android/thirdparty/com/adjust"
+            }
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    compile 'com.android.support:support-v4:21.+'
+}
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -98,16 +98,19 @@ class MachCommands(MachCommandBase):
         srcdir('preprocessed_resources/build.gradle', 'mobile/android/gradle/preprocessed_resources/build.gradle')
         srcdir('preprocessed_resources/src/main/AndroidManifest.xml', 'mobile/android/gradle/preprocessed_resources/AndroidManifest.xml')
         objdir('preprocessed_resources/src/main/res', 'mobile/android/base/res')
 
         srcdir('thirdparty/build.gradle', 'mobile/android/gradle/thirdparty/build.gradle')
         srcdir('thirdparty/src/main/AndroidManifest.xml', 'mobile/android/gradle/thirdparty/AndroidManifest.xml')
         srcdir('thirdparty/src/main/java', 'mobile/android/thirdparty')
 
+        srcdir('thirdparty_adjust_sdk/build.gradle', 'mobile/android/gradle/thirdparty_adjust_sdk/build.gradle')
+        srcdir('thirdparty_adjust_sdk/src/main/AndroidManifest.xml', 'mobile/android/gradle/thirdparty_adjust_sdk/AndroidManifest.xml')
+
         srcdir('omnijar/build.gradle', 'mobile/android/gradle/omnijar/build.gradle')
         srcdir('omnijar/src/main/java/locales', 'mobile/android/locales')
         srcdir('omnijar/src/main/java/chrome', 'mobile/android/chrome')
         srcdir('omnijar/src/main/java/components', 'mobile/android/components')
         srcdir('omnijar/src/main/java/modules', 'mobile/android/modules')
         srcdir('omnijar/src/main/java/themes', 'mobile/android/themes')
 
         srcdir('app/build.gradle', 'mobile/android/gradle/app/build.gradle')
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
@@ -0,0 +1,759 @@
+//
+//  ActivityHandler.java
+//  Adjust
+//
+//  Created by Christian Wellenbrock on 2013-06-25.
+//  Copyright (c) 2013 adjust GmbH. All rights reserved.
+//  See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static com.adjust.sdk.Constants.ACTIVITY_STATE_FILENAME;
+import static com.adjust.sdk.Constants.ATTRIBUTION_FILENAME;
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class ActivityHandler extends HandlerThread implements IActivityHandler {
+
+    private static long TIMER_INTERVAL;
+    private static long TIMER_START;
+    private static long SESSION_INTERVAL;
+    private static long SUBSESSION_INTERVAL;
+    private static final String TIME_TRAVEL = "Time travel!";
+    private static final String ADJUST_PREFIX = "adjust_";
+    private static final String ACTIVITY_STATE_NAME = "Activity state";
+    private static final String ATTRIBUTION_NAME = "Attribution";
+
+    private SessionHandler sessionHandler;
+    private IPackageHandler packageHandler;
+    private ActivityState activityState;
+    private ILogger logger;
+    private static ScheduledExecutorService timer;
+    private boolean enabled;
+    private boolean offline;
+
+    private DeviceInfo deviceInfo;
+    private AdjustConfig adjustConfig; // always valid after construction
+    private AdjustAttribution attribution;
+    private IAttributionHandler attributionHandler;
+
+    private ActivityHandler(AdjustConfig adjustConfig) {
+        super(LOGTAG, MIN_PRIORITY);
+        setDaemon(true);
+        start();
+
+        logger = AdjustFactory.getLogger();
+        sessionHandler = new SessionHandler(getLooper(), this);
+        enabled = true;
+        init(adjustConfig);
+
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.INIT;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public void init(AdjustConfig adjustConfig) {
+        this.adjustConfig = adjustConfig;
+    }
+
+    public static ActivityHandler getInstance(AdjustConfig adjustConfig) {
+        if (adjustConfig == null) {
+            AdjustFactory.getLogger().error("AdjustConfig missing");
+            return null;
+        }
+
+        if (!adjustConfig.isValid()) {
+            AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
+            return null;
+        }
+
+        ActivityHandler activityHandler = new ActivityHandler(adjustConfig);
+        return activityHandler;
+    }
+
+    @Override
+    public void trackSubsessionStart() {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.START;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public void trackSubsessionEnd() {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.END;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public void trackEvent(AdjustEvent event) {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.EVENT;
+        message.obj = event;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public void finishedTrackingActivity(JSONObject jsonResponse) {
+        if (jsonResponse == null) {
+            return;
+        }
+
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.FINISH_TRACKING;
+        message.obj = jsonResponse;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (enabled == this.enabled) {
+            if (enabled) {
+                logger.debug("Adjust already enabled");
+            } else {
+                logger.debug("Adjust already disabled");
+            }
+            return;
+        }
+        this.enabled = enabled;
+        if (activityState != null) {
+            activityState.enabled = enabled;
+        }
+        if (enabled) {
+            if (toPause()) {
+                logger.info("Package and attribution handler remain paused due to the SDK is offline");
+            } else {
+                logger.info("Resuming package handler and attribution handler to enabled the SDK");
+            }
+            trackSubsessionStart();
+        } else {
+            logger.info("Pausing package handler and attribution handler to disable the SDK");
+            trackSubsessionEnd();
+        }
+    }
+
+    @Override
+    public void setOfflineMode(boolean offline) {
+        if (offline == this.offline) {
+            if (offline) {
+                logger.debug("Adjust already in offline mode");
+            } else {
+                logger.debug("Adjust already in online mode");
+            }
+            return;
+        }
+        this.offline = offline;
+        if (offline) {
+            logger.info("Pausing package and attribution handler to put in offline mode");
+        } else {
+            if (toPause()) {
+                logger.info("Package and attribution handler remain paused because the SDK is disabled");
+            } else {
+                logger.info("Resuming package handler and attribution handler to put in online mode");
+            }
+        }
+        updateStatus();
+    }
+
+    @Override
+    public boolean isEnabled() {
+        if (activityState != null) {
+            return activityState.enabled;
+        } else {
+            return enabled;
+        }
+    }
+
+    @Override
+    public void readOpenUrl(Uri url, long clickTime) {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.DEEP_LINK;
+        UrlClickTime urlClickTime = new UrlClickTime(url, clickTime);
+        message.obj = urlClickTime;
+        sessionHandler.sendMessage(message);
+    }
+
+    @Override
+    public boolean tryUpdateAttribution(AdjustAttribution attribution) {
+        if (attribution == null) return false;
+
+        if (attribution.equals(this.attribution)) {
+            return false;
+        }
+
+        saveAttribution(attribution);
+        launchAttributionListener();
+        return true;
+    }
+
+    private void saveAttribution(AdjustAttribution attribution) {
+        this.attribution = attribution;
+        writeAttribution();
+    }
+
+    private void launchAttributionListener() {
+        if (adjustConfig.onAttributionChangedListener == null) {
+            return;
+        }
+        Handler handler = new Handler(adjustConfig.context.getMainLooper());
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                adjustConfig.onAttributionChangedListener.onAttributionChanged(attribution);
+            }
+        };
+        handler.post(runnable);
+    }
+
+    @Override
+    public void setAskingAttribution(boolean askingAttribution) {
+        activityState.askingAttribution = askingAttribution;
+        writeActivityState();
+    }
+
+    @Override
+    public ActivityPackage getAttributionPackage() {
+        long now = System.currentTimeMillis();
+        PackageBuilder attributionBuilder = new PackageBuilder(adjustConfig,
+                deviceInfo,
+                activityState,
+                now);
+        return attributionBuilder.buildAttributionPackage();
+    }
+
+    @Override
+    public void sendReferrer(String referrer, long clickTime) {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.SEND_REFERRER;
+        ReferrerClickTime referrerClickTime = new ReferrerClickTime(referrer, clickTime);
+        message.obj = referrerClickTime;
+        sessionHandler.sendMessage(message);
+    }
+
+    private class UrlClickTime {
+        Uri url;
+        long clickTime;
+
+        UrlClickTime(Uri url, long clickTime) {
+            this.url = url;
+            this.clickTime = clickTime;
+        }
+    }
+
+    private class ReferrerClickTime {
+        String referrer;
+        long clickTime;
+
+        ReferrerClickTime(String referrer, long clickTime) {
+            this.referrer = referrer;
+            this.clickTime = clickTime;
+        }
+    }
+
+    private void updateStatus() {
+        Message message = Message.obtain();
+        message.arg1 = SessionHandler.UPDATE_STATUS;
+        sessionHandler.sendMessage(message);
+    }
+
+    private static final class SessionHandler extends Handler {
+        private static final int BASE_ADDRESS = 72630;
+        private static final int INIT = BASE_ADDRESS + 1;
+        private static final int START = BASE_ADDRESS + 2;
+        private static final int END = BASE_ADDRESS + 3;
+        private static final int EVENT = BASE_ADDRESS + 4;
+        private static final int FINISH_TRACKING = BASE_ADDRESS + 5;
+        private static final int DEEP_LINK = BASE_ADDRESS + 6;
+        private static final int SEND_REFERRER = BASE_ADDRESS + 7;
+        private static final int UPDATE_STATUS = BASE_ADDRESS + 8;
+
+        private final WeakReference<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() {
+        activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+    }
+
+    private void readAttribution() {
+        attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+    }
+
+    private void writeActivityState() {
+        Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+    }
+
+    private void writeAttribution() {
+        Util.writeObject(attribution, adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+    }
+
+    private boolean checkEvent(AdjustEvent event) {
+        if (event == null) {
+            logger.error("Event missing");
+            return false;
+        }
+
+        if (!event.isValid()) {
+            logger.error("Event not initialized correctly");
+            return false;
+        }
+
+        return true;
+    }
+
+    // lazy initialization to prevent null activity state before first session
+    private IAttributionHandler getAttributionHandler() {
+        if (attributionHandler == null) {
+            ActivityPackage attributionPackage = getAttributionPackage();
+            attributionHandler = AdjustFactory.getAttributionHandler(this,
+                    attributionPackage,
+                    toPause());
+        }
+        return attributionHandler;
+    }
+
+    private boolean toPause() {
+        return offline || !isEnabled();
+    }
+}
new file mode 100644
--- /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";
+        }
+    }
+}
new file mode 100644
--- /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 ???";
+        }
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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);
+    }
+}
+
+
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
@@ -0,0 +1,141 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpParams;
+
+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;
+    }
+
+}
new file mode 100644
--- /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;
+        }
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
@@ -0,0 +1,155 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AttributionHandler implements IAttributionHandler {
+    private ScheduledExecutorService scheduler;
+    private IActivityHandler activityHandler;
+    private ILogger logger;
+    private ActivityPackage attributionPackage;
+    private ScheduledFuture waitingTask;
+    private HttpClient httpClient;
+    private boolean paused;
+
+    public AttributionHandler(IActivityHandler activityHandler,
+                              ActivityPackage attributionPackage,
+                              boolean startPaused) {
+        scheduler = Executors.newSingleThreadScheduledExecutor();
+        logger = AdjustFactory.getLogger();
+        httpClient = Util.getHttpClient();
+        init(activityHandler, attributionPackage, startPaused);
+    }
+
+    @Override
+    public void init(IActivityHandler activityHandler,
+                     ActivityPackage attributionPackage,
+                     boolean startPaused) {
+        this.activityHandler = activityHandler;
+        this.attributionPackage = attributionPackage;
+        this.paused = startPaused;
+    }
+
+    @Override
+    public void getAttribution() {
+        getAttribution(0);
+    }
+
+    @Override
+    public void checkAttribution(final JSONObject jsonResponse) {
+        scheduler.submit(new Runnable() {
+            @Override
+            public void run() {
+                checkAttributionInternal(jsonResponse);
+            }
+        });
+    }
+
+    @Override
+    public void pauseSending() {
+        paused = true;
+    }
+
+    @Override
+    public void resumeSending() {
+        paused = false;
+    }
+
+    private void getAttribution(int delayInMilliseconds) {
+        if (waitingTask != null) {
+            waitingTask.cancel(false);
+        }
+
+        if (delayInMilliseconds != 0) {
+            logger.debug("Waiting to query attribution in %d milliseconds", delayInMilliseconds);
+        }
+
+        waitingTask = scheduler.schedule(new Runnable() {
+            @Override
+            public void run() {
+                getAttributionInternal();
+            }
+        }, delayInMilliseconds, TimeUnit.MILLISECONDS);
+    }
+
+    private void checkAttributionInternal(JSONObject jsonResponse) {
+        if (jsonResponse == null) return;
+
+        JSONObject attributionJson = jsonResponse.optJSONObject("attribution");
+        AdjustAttribution attribution = AdjustAttribution.fromJson(attributionJson);
+
+        int timerMilliseconds = jsonResponse.optInt("ask_in", -1);
+
+        // without ask_in attribute
+        if (timerMilliseconds < 0) {
+            activityHandler.tryUpdateAttribution(attribution);
+
+            activityHandler.setAskingAttribution(false);
+
+            return;
+        }
+
+        activityHandler.setAskingAttribution(true);
+
+        getAttribution(timerMilliseconds);
+    }
+
+    private void getAttributionInternal() {
+        if (paused) {
+            logger.debug("Attribution Handler is paused");
+            return;
+        }
+        logger.verbose("%s", attributionPackage.getExtendedString());
+        HttpResponse httpResponse = null;
+        try {
+            HttpGet request = getRequest(attributionPackage);
+            httpResponse = httpClient.execute(request);
+        } catch (Exception e) {
+            logger.error("Failed to get attribution (%s)", e.getMessage());
+            return;
+        }
+
+        JSONObject jsonResponse = Util.parseJsonResponse(httpResponse, logger);
+
+        checkAttributionInternal(jsonResponse);
+    }
+
+    private Uri buildUri(ActivityPackage attributionPackage) {
+        Uri.Builder uriBuilder = new Uri.Builder();
+
+        uriBuilder.scheme(Constants.SCHEME);
+        uriBuilder.authority(Constants.AUTHORITY);
+        uriBuilder.appendPath(attributionPackage.getPath());
+
+        for (Map.Entry<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;
+    }
+}
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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;
+        }
+    }
+}
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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);
+
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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)));
+        }
+    }
+}
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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();
+        }
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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 org.apache.http.HttpResponse;
+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.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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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 org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Collects utility functions used by Adjust.
+ */
+public class Util {
+
+    private static SimpleDateFormat dateFormat;
+    private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z";
+
+    protected static String createUuid() {
+        return UUID.randomUUID().toString();
+    }
+
+    public static String quote(String string) {
+        if (string == null) {
+            return null;
+        }
+
+        Pattern pattern = Pattern.compile("\\s");
+        Matcher matcher = pattern.matcher(string);
+        if (!matcher.find()) {
+            return string;
+        }
+
+        return String.format("'%s'", string);
+    }
+
+    public static String dateFormat(long date) {
+        if (null == dateFormat) {
+            dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+        }
+        return dateFormat.format(date);
+    }
+
+    public static String getPlayAdId(Context context) {
+        return Reflection.getPlayAdId(context);
+    }
+
+    public static Boolean isPlayTrackingEnabled(Context context) {
+        return Reflection.isPlayTrackingEnabled(context);
+    }
+
+    public static <T> 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;
+    }
+}
new file mode 100644
--- /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);
+    }
+}
new file mode 100644
--- /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;
+    }
+}
new file mode 100644
--- /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);
+}