diff options
Diffstat (limited to 'mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java')
-rw-r--r-- | mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java | 781 |
1 files changed, 781 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java new file mode 100644 index 000000000..8c7858b97 --- /dev/null +++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java @@ -0,0 +1,781 @@ +// +// ActivityHandler.java +// Adjust +// +// Created by Christian Wellenbrock on 2013-06-25. +// Copyright (c) 2013 adjust GmbH. All rights reserved. +// See the file MIT-LICENSE for copying permission. +// + +package com.adjust.sdk; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.adjust.sdk.Constants.ACTIVITY_STATE_FILENAME; +import static com.adjust.sdk.Constants.ATTRIBUTION_FILENAME; +import static com.adjust.sdk.Constants.LOGTAG; + +public class ActivityHandler extends HandlerThread implements IActivityHandler { + + private static long TIMER_INTERVAL; + private static long TIMER_START; + private static long SESSION_INTERVAL; + private static long SUBSESSION_INTERVAL; + private static final String TIME_TRAVEL = "Time travel!"; + private static final String ADJUST_PREFIX = "adjust_"; + private static final String ACTIVITY_STATE_NAME = "Activity state"; + private static final String ATTRIBUTION_NAME = "Attribution"; + + private SessionHandler sessionHandler; + private IPackageHandler packageHandler; + private ActivityState activityState; + private ILogger logger; + private static ScheduledExecutorService timer; + private boolean enabled; + private boolean offline; + + private DeviceInfo deviceInfo; + private AdjustConfig adjustConfig; // always valid after construction + private AdjustAttribution attribution; + private IAttributionHandler attributionHandler; + + private ActivityHandler(AdjustConfig adjustConfig) { + super(LOGTAG, MIN_PRIORITY); + setDaemon(true); + start(); + + logger = AdjustFactory.getLogger(); + sessionHandler = new SessionHandler(getLooper(), this); + enabled = true; + init(adjustConfig); + + Message message = Message.obtain(); + message.arg1 = SessionHandler.INIT; + sessionHandler.sendMessage(message); + } + + @Override + public void init(AdjustConfig adjustConfig) { + this.adjustConfig = adjustConfig; + } + + public static ActivityHandler getInstance(AdjustConfig adjustConfig) { + if (adjustConfig == null) { + AdjustFactory.getLogger().error("AdjustConfig missing"); + return null; + } + + if (!adjustConfig.isValid()) { + AdjustFactory.getLogger().error("AdjustConfig not initialized correctly"); + return null; + } + + ActivityHandler activityHandler = new ActivityHandler(adjustConfig); + return activityHandler; + } + + @Override + public void trackSubsessionStart() { + Message message = Message.obtain(); + message.arg1 = SessionHandler.START; + sessionHandler.sendMessage(message); + } + + @Override + public void trackSubsessionEnd() { + Message message = Message.obtain(); + message.arg1 = SessionHandler.END; + sessionHandler.sendMessage(message); + } + + @Override + public void trackEvent(AdjustEvent event) { + Message message = Message.obtain(); + message.arg1 = SessionHandler.EVENT; + message.obj = event; + sessionHandler.sendMessage(message); + } + + @Override + public void finishedTrackingActivity(JSONObject jsonResponse) { + if (jsonResponse == null) { + return; + } + + Message message = Message.obtain(); + message.arg1 = SessionHandler.FINISH_TRACKING; + message.obj = jsonResponse; + sessionHandler.sendMessage(message); + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled == this.enabled) { + if (enabled) { + logger.debug("Adjust already enabled"); + } else { + logger.debug("Adjust already disabled"); + } + return; + } + this.enabled = enabled; + if (activityState != null) { + activityState.enabled = enabled; + } + if (enabled) { + if (toPause()) { + logger.info("Package and attribution handler remain paused due to the SDK is offline"); + } else { + logger.info("Resuming package handler and attribution handler to enabled the SDK"); + } + trackSubsessionStart(); + } else { + logger.info("Pausing package handler and attribution handler to disable the SDK"); + trackSubsessionEnd(); + } + } + + @Override + public void setOfflineMode(boolean offline) { + if (offline == this.offline) { + if (offline) { + logger.debug("Adjust already in offline mode"); + } else { + logger.debug("Adjust already in online mode"); + } + return; + } + this.offline = offline; + if (offline) { + logger.info("Pausing package and attribution handler to put in offline mode"); + } else { + if (toPause()) { + logger.info("Package and attribution handler remain paused because the SDK is disabled"); + } else { + logger.info("Resuming package handler and attribution handler to put in online mode"); + } + } + updateStatus(); + } + + @Override + public boolean isEnabled() { + if (activityState != null) { + return activityState.enabled; + } else { + return enabled; + } + } + + @Override + public void readOpenUrl(Uri url, long clickTime) { + Message message = Message.obtain(); + message.arg1 = SessionHandler.DEEP_LINK; + UrlClickTime urlClickTime = new UrlClickTime(url, clickTime); + message.obj = urlClickTime; + sessionHandler.sendMessage(message); + } + + @Override + public boolean tryUpdateAttribution(AdjustAttribution attribution) { + if (attribution == null) return false; + + if (attribution.equals(this.attribution)) { + return false; + } + + saveAttribution(attribution); + launchAttributionListener(); + return true; + } + + private void saveAttribution(AdjustAttribution attribution) { + this.attribution = attribution; + writeAttribution(); + } + + private void launchAttributionListener() { + if (adjustConfig.onAttributionChangedListener == null) { + return; + } + Handler handler = new Handler(adjustConfig.context.getMainLooper()); + Runnable runnable = new Runnable() { + @Override + public void run() { + adjustConfig.onAttributionChangedListener.onAttributionChanged(attribution); + } + }; + handler.post(runnable); + } + + @Override + public void setAskingAttribution(boolean askingAttribution) { + activityState.askingAttribution = askingAttribution; + writeActivityState(); + } + + @Override + public ActivityPackage getAttributionPackage() { + long now = System.currentTimeMillis(); + PackageBuilder attributionBuilder = new PackageBuilder(adjustConfig, + deviceInfo, + activityState, + now); + return attributionBuilder.buildAttributionPackage(); + } + + @Override + public void sendReferrer(String referrer, long clickTime) { + Message message = Message.obtain(); + message.arg1 = SessionHandler.SEND_REFERRER; + ReferrerClickTime referrerClickTime = new ReferrerClickTime(referrer, clickTime); + message.obj = referrerClickTime; + sessionHandler.sendMessage(message); + } + + private class UrlClickTime { + Uri url; + long clickTime; + + UrlClickTime(Uri url, long clickTime) { + this.url = url; + this.clickTime = clickTime; + } + } + + private class ReferrerClickTime { + String referrer; + long clickTime; + + ReferrerClickTime(String referrer, long clickTime) { + this.referrer = referrer; + this.clickTime = clickTime; + } + } + + private void updateStatus() { + Message message = Message.obtain(); + message.arg1 = SessionHandler.UPDATE_STATUS; + sessionHandler.sendMessage(message); + } + + private static final class SessionHandler extends Handler { + private static final int BASE_ADDRESS = 72630; + private static final int INIT = BASE_ADDRESS + 1; + private static final int START = BASE_ADDRESS + 2; + private static final int END = BASE_ADDRESS + 3; + private static final int EVENT = BASE_ADDRESS + 4; + private static final int FINISH_TRACKING = BASE_ADDRESS + 5; + private static final int DEEP_LINK = BASE_ADDRESS + 6; + private static final int SEND_REFERRER = BASE_ADDRESS + 7; + private static final int UPDATE_STATUS = BASE_ADDRESS + 8; + + private final WeakReference<ActivityHandler> sessionHandlerReference; + + protected SessionHandler(Looper looper, ActivityHandler sessionHandler) { + super(looper); + this.sessionHandlerReference = new WeakReference<ActivityHandler>(sessionHandler); + } + + @Override + public void handleMessage(Message message) { + super.handleMessage(message); + + ActivityHandler sessionHandler = sessionHandlerReference.get(); + if (sessionHandler == null) { + return; + } + + switch (message.arg1) { + case INIT: + sessionHandler.initInternal(); + break; + case START: + sessionHandler.startInternal(); + break; + case END: + sessionHandler.endInternal(); + break; + case EVENT: + AdjustEvent event = (AdjustEvent) message.obj; + sessionHandler.trackEventInternal(event); + break; + case FINISH_TRACKING: + JSONObject jsonResponse = (JSONObject) message.obj; + sessionHandler.finishedTrackingActivityInternal(jsonResponse); + break; + case DEEP_LINK: + UrlClickTime urlClickTime = (UrlClickTime) message.obj; + sessionHandler.readOpenUrlInternal(urlClickTime.url, urlClickTime.clickTime); + break; + case SEND_REFERRER: + ReferrerClickTime referrerClickTime = (ReferrerClickTime) message.obj; + sessionHandler.sendReferrerInternal(referrerClickTime.referrer, referrerClickTime.clickTime); + break; + case UPDATE_STATUS: + sessionHandler.updateStatusInternal(); + break; + } + } + } + + private void initInternal() { + TIMER_INTERVAL = AdjustFactory.getTimerInterval(); + TIMER_START = AdjustFactory.getTimerStart(); + SESSION_INTERVAL = AdjustFactory.getSessionInterval(); + SUBSESSION_INTERVAL = AdjustFactory.getSubsessionInterval(); + + deviceInfo = new DeviceInfo(adjustConfig.context, adjustConfig.sdkPrefix); + + if (adjustConfig.environment == AdjustConfig.ENVIRONMENT_PRODUCTION) { + logger.setLogLevel(LogLevel.ASSERT); + } else { + logger.setLogLevel(adjustConfig.logLevel); + } + + if (adjustConfig.eventBufferingEnabled) { + logger.info("Event buffering is enabled"); + } + + String playAdId = Util.getPlayAdId(adjustConfig.context); + if (playAdId == null) { + logger.info("Unable to get Google Play Services Advertising ID at start time"); + } + + if (adjustConfig.defaultTracker != null) { + logger.info("Default tracker: '%s'", adjustConfig.defaultTracker); + } + + if (adjustConfig.referrer != null) { + sendReferrer(adjustConfig.referrer, adjustConfig.referrerClickTime); // send to background queue to make sure that activityState is valid + } + + readAttribution(); + readActivityState(); + + packageHandler = AdjustFactory.getPackageHandler(this, adjustConfig.context, toPause()); + + startInternal(); + } + + private void startInternal() { + // it shouldn't start if it was disabled after a first session + if (activityState != null + && !activityState.enabled) { + return; + } + + updateStatusInternal(); + + processSession(); + + checkAttributionState(); + + startTimer(); + } + + private void processSession() { + long now = System.currentTimeMillis(); + + // very first session + if (activityState == null) { + activityState = new ActivityState(); + activityState.sessionCount = 1; // this is the first session + + transferSessionPackage(now); + activityState.resetSessionAttributes(now); + activityState.enabled = this.enabled; + writeActivityState(); + return; + } + + long lastInterval = now - activityState.lastActivity; + + if (lastInterval < 0) { + logger.error(TIME_TRAVEL); + activityState.lastActivity = now; + writeActivityState(); + return; + } + + // new session + if (lastInterval > SESSION_INTERVAL) { + activityState.sessionCount++; + activityState.lastInterval = lastInterval; + + transferSessionPackage(now); + activityState.resetSessionAttributes(now); + writeActivityState(); + return; + } + + // new subsession + if (lastInterval > SUBSESSION_INTERVAL) { + activityState.subsessionCount++; + activityState.sessionLength += lastInterval; + activityState.lastActivity = now; + writeActivityState(); + logger.info("Started subsession %d of session %d", + activityState.subsessionCount, + activityState.sessionCount); + } + } + + private void checkAttributionState() { + // if there is no attribution saved, or there is one being asked + if (attribution == null || activityState.askingAttribution) { + getAttributionHandler().getAttribution(); + } + } + + private void endInternal() { + packageHandler.pauseSending(); + getAttributionHandler().pauseSending(); + stopTimer(); + if (updateActivityState(System.currentTimeMillis())) { + writeActivityState(); + } + } + + private void trackEventInternal(AdjustEvent event) { + if (!checkEvent(event)) return; + if (!activityState.enabled) return; + + long now = System.currentTimeMillis(); + + activityState.eventCount++; + updateActivityState(now); + + PackageBuilder eventBuilder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now); + ActivityPackage eventPackage = eventBuilder.buildEventPackage(event); + packageHandler.addPackage(eventPackage); + + if (adjustConfig.eventBufferingEnabled) { + logger.info("Buffered event %s", eventPackage.getSuffix()); + } else { + packageHandler.sendFirstPackage(); + } + + writeActivityState(); + } + + private void finishedTrackingActivityInternal(JSONObject jsonResponse) { + if (jsonResponse == null) { + return; + } + + String deeplink = jsonResponse.optString("deeplink", null); + launchDeeplinkMain(deeplink); + getAttributionHandler().checkAttribution(jsonResponse); + } + + private void sendReferrerInternal(String referrer, long clickTime) { + ActivityPackage clickPackage = buildQueryStringClickPackage(referrer, + "reftag", + clickTime); + if (clickPackage == null) { + return; + } + + getAttributionHandler().getAttribution(); + + packageHandler.sendClickPackage(clickPackage); + } + + private void readOpenUrlInternal(Uri url, long clickTime) { + if (url == null) { + return; + } + + String queryString = url.getQuery(); + + ActivityPackage clickPackage = buildQueryStringClickPackage(queryString, "deeplink", clickTime); + if (clickPackage == null) { + return; + } + + getAttributionHandler().getAttribution(); + + packageHandler.sendClickPackage(clickPackage); + } + + private ActivityPackage buildQueryStringClickPackage(String queryString, String source, long clickTime) { + if (queryString == null) { + return null; + } + + long now = System.currentTimeMillis(); + Map<String, String> queryStringParameters = new HashMap<String, String>(); + AdjustAttribution queryStringAttribution = new AdjustAttribution(); + boolean hasAdjustTags = false; + + String[] queryPairs = queryString.split("&"); + for (String pair : queryPairs) { + if (readQueryString(pair, queryStringParameters, queryStringAttribution)) { + hasAdjustTags = true; + } + } + + if (!hasAdjustTags) { + return null; + } + + String reftag = queryStringParameters.remove("reftag"); + + PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now); + builder.extraParameters = queryStringParameters; + builder.attribution = queryStringAttribution; + builder.reftag = reftag; + ActivityPackage clickPackage = builder.buildClickPackage(source, clickTime); + return clickPackage; + } + + private boolean readQueryString(String queryString, + Map<String, String> extraParameters, + AdjustAttribution queryStringAttribution) { + String[] pairComponents = queryString.split("="); + if (pairComponents.length != 2) return false; + + String key = pairComponents[0]; + if (!key.startsWith(ADJUST_PREFIX)) return false; + + String value = pairComponents[1]; + if (value.length() == 0) return false; + + String keyWOutPrefix = key.substring(ADJUST_PREFIX.length()); + if (keyWOutPrefix.length() == 0) return false; + + if (!trySetAttribution(queryStringAttribution, keyWOutPrefix, value)) { + extraParameters.put(keyWOutPrefix, value); + } + + return true; + } + + private boolean trySetAttribution(AdjustAttribution queryStringAttribution, + String key, + String value) { + if (key.equals("tracker")) { + queryStringAttribution.trackerName = value; + return true; + } + + if (key.equals("campaign")) { + queryStringAttribution.campaign = value; + return true; + } + + if (key.equals("adgroup")) { + queryStringAttribution.adgroup = value; + return true; + } + + if (key.equals("creative")) { + queryStringAttribution.creative = value; + return true; + } + + return false; + } + + private void updateStatusInternal() { + updateAttributionHandlerStatus(); + updatePackageHandlerStatus(); + } + + private void updateAttributionHandlerStatus() { + if (attributionHandler == null) { + return; + } + if (toPause()) { + attributionHandler.pauseSending(); + } else { + attributionHandler.resumeSending(); + } + } + + private void updatePackageHandlerStatus() { + if (packageHandler == null) { + return; + } + if (toPause()) { + packageHandler.pauseSending(); + } else { + packageHandler.resumeSending(); + } + } + + private void launchDeeplinkMain(String deeplink) { + if (deeplink == null) return; + + Uri location = Uri.parse(deeplink); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, location); + mapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Verify it resolves + PackageManager packageManager = adjustConfig.context.getPackageManager(); + List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0); + boolean isIntentSafe = activities.size() > 0; + + // Start an activity if it's safe + if (!isIntentSafe) { + logger.error("Unable to open deep link (%s)", deeplink); + return; + } + + logger.info("Open deep link (%s)", deeplink); + adjustConfig.context.startActivity(mapIntent); + } + + private boolean updateActivityState(long now) { + long lastInterval = now - activityState.lastActivity; + // ignore late updates + if (lastInterval > SESSION_INTERVAL) { + return false; + } + activityState.lastActivity = now; + + if (lastInterval < 0) { + logger.error(TIME_TRAVEL); + } else { + activityState.sessionLength += lastInterval; + activityState.timeSpent += lastInterval; + } + return true; + } + + public static boolean deleteActivityState(Context context) { + return context.deleteFile(ACTIVITY_STATE_FILENAME); + } + + public static boolean deleteAttribution(Context context) { + return context.deleteFile(ATTRIBUTION_FILENAME); + } + + private void transferSessionPackage(long now) { + PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now); + ActivityPackage sessionPackage = builder.buildSessionPackage(); + packageHandler.addPackage(sessionPackage); + packageHandler.sendFirstPackage(); + } + + private void startTimer() { + stopTimer(); + + if (!activityState.enabled) { + return; + } + timer = Executors.newSingleThreadScheduledExecutor(); + timer.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + timerFired(); + } + }, TIMER_START, TIMER_INTERVAL, TimeUnit.MILLISECONDS); + } + + private void stopTimer() { + if (timer != null) { + timer.shutdown(); + timer = null; + } + } + + private void timerFired() { + if (!activityState.enabled) { + stopTimer(); + return; + } + + packageHandler.sendFirstPackage(); + + if (updateActivityState(System.currentTimeMillis())) { + writeActivityState(); + } + } + + private void readActivityState() { + try { + /** + * Mozilla: + * readObject is a generic object, and can therefore return arbitrary generic objects + * that might not match the expected type. Therefore there will be an implicit cast + * here, which can fail. Therefore we have to add the catch (ClassCastException) + * Note: this has been fixed in upstream, we only need this for the version we are still shipping. + */ + activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME); + } catch (ClassCastException e) { + activityState = null; + } + } + + private void readAttribution() { + try { + /** + * Mozilla: (same as in readActivityState() ) + * readObject is a generic object, and can therefore return arbitrary generic objects + * that might not match the expected type. Therefore there will be an implicit cast + * here, which can fail. Therefore we have to add the catch (ClassCastException) + * Note: this has been fixed in upstream, we only need this for the version we are still shipping. + */ + attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME); + } catch (ClassCastException e) { + activityState = null; + } + } + + private void writeActivityState() { + Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME); + } + + private void writeAttribution() { + Util.writeObject(attribution, adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME); + } + + private boolean checkEvent(AdjustEvent event) { + if (event == null) { + logger.error("Event missing"); + return false; + } + + if (!event.isValid()) { + logger.error("Event not initialized correctly"); + return false; + } + + return true; + } + + // lazy initialization to prevent null activity state before first session + private IAttributionHandler getAttributionHandler() { + if (attributionHandler == null) { + ActivityPackage attributionPackage = getAttributionPackage(); + attributionHandler = AdjustFactory.getAttributionHandler(this, + attributionPackage, + toPause()); + } + return attributionHandler; + } + + private boolean toPause() { + return offline || !isEnabled(); + } +} |