Java tutorial
// // 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.app.ActivityManager; 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.LinkedHashMap; import java.util.List; import java.util.Map; 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 TimerCycle 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; } if (adjustConfig.processName != null) { int currentPid = android.os.Process.myPid(); ActivityManager manager = (ActivityManager) adjustConfig.context .getSystemService(Context.ACTIVITY_SERVICE); if (manager == null) { return null; } for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) { if (processInfo.pid == currentPid) { if (!processInfo.processName.equalsIgnoreCase(adjustConfig.processName)) { AdjustFactory.getLogger().info("Skipping initialization in background process (%s)", processInfo.processName); return null; } break; } } } 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) { if (activityState == null) { trackSubsessionStart(); } 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 (!hasChangedState(isEnabled(), enabled, "Adjust already enabled", "Adjust already disabled")) { return; } this.enabled = enabled; if (activityState == null) { trackSubsessionStart(); } else { activityState.enabled = enabled; writeActivityState(); } updateStatus(!enabled, "Pausing package handler and attribution handler to disable the SDK", "Package and attribution handler remain paused due to the SDK is offline", "Resuming package handler and attribution handler to enabled the SDK"); } private void updateStatus(boolean pausingState, String pausingMessage, String remainsPausedMessage, String unPausingMessage) { if (pausingState) { logger.info(pausingMessage); trackSubsessionEnd(); return; } if (paused()) { logger.info(remainsPausedMessage); } else { logger.info(unPausingMessage); trackSubsessionStart(); } } private boolean hasChangedState(boolean previousState, boolean newState, String trueMessage, String falseMessage) { if (previousState != newState) { return true; } if (previousState) { logger.debug(trueMessage); } else { logger.debug(falseMessage); } return false; } @Override public void setOfflineMode(boolean offline) { if (!hasChangedState(this.offline, offline, "Adjust already in offline mode", "Adjust already in online mode")) { return; } this.offline = offline; if (activityState == null) { trackSubsessionStart(); } updateStatus(offline, "Pausing package and attribution handler to put in offline mode", "Package and attribution handler remain paused because the SDK is disabled", "Resuming package handler and attribution handler to put in online mode"); } @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 void timerFired() { Message message = Message.obtain(); message.arg1 = SessionHandler.TIMER_FIRED; 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 static final int TIMER_FIRED = BASE_ADDRESS + 9; 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; case TIMER_FIRED: sessionHandler.timerFiredInternal(); 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_PRODUCTION.equals(adjustConfig.environment)) { 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, paused()); ActivityPackage attributionPackage = getAttributionPackage(); attributionHandler = AdjustFactory.getAttributionHandler(this, attributionPackage, paused(), adjustConfig.hasListener()); timer = new TimerCycle(new Runnable() { @Override public void run() { timerFired(); } }, TIMER_START, TIMER_INTERVAL); } 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 (!checkActivityState(activityState)) { return; } // if it's a new session if (activityState.subsessionCount <= 1) { return; } // if there is already an attribution saved and there was no attribution being asked if (attribution != null && !activityState.askingAttribution) { return; } attributionHandler.getAttribution(); } private void endInternal() { packageHandler.pauseSending(); attributionHandler.pauseSending(); stopTimer(); if (updateActivityState(System.currentTimeMillis())) { writeActivityState(); } } private void trackEventInternal(AdjustEvent event) { if (!checkActivityState(activityState)) return; if (!isEnabled()) return; if (!checkEvent(event)) 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); attributionHandler.checkAttribution(jsonResponse); } private void sendReferrerInternal(String referrer, long clickTime) { ActivityPackage clickPackage = buildQueryStringClickPackage(referrer, Constants.REFTAG, clickTime); if (clickPackage == null) { return; } packageHandler.addPackage(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; } packageHandler.addPackage(clickPackage); } private ActivityPackage buildQueryStringClickPackage(String queryString, String source, long clickTime) { if (queryString == null) { return null; } Map<String, String> queryStringParameters = new LinkedHashMap<String, String>(); AdjustAttribution queryStringAttribution = new AdjustAttribution(); boolean hasAdjustTags = false; logger.verbose("Reading query string (%s) from %s", queryString, source); String[] queryPairs = queryString.split("&"); for (String pair : queryPairs) { if (readQueryString(pair, queryStringParameters, queryStringAttribution)) { hasAdjustTags = true; } } if (!hasAdjustTags) { return null; } String reftag = queryStringParameters.remove(Constants.REFTAG); long now = System.currentTimeMillis(); PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now); builder.extraParameters = queryStringParameters; builder.attribution = queryStringAttribution; builder.reftag = reftag; if (source == Constants.REFTAG) { builder.referrer = queryString; } 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 (paused()) { attributionHandler.pauseSending(); } else { attributionHandler.resumeSending(); } } private void updatePackageHandlerStatus() { if (paused()) { 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) { if (!checkActivityState(activityState)) { return false; } 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() { // don't start the timer if it's disabled/offline if (paused()) { return; } timer.start(); } private void stopTimer() { timer.suspend(); } private void timerFiredInternal() { if (paused()) { // stop the timer cycle if it's disabled/offline stopTimer(); return; } logger.debug("Session timer fired"); packageHandler.sendFirstPackage(); if (updateActivityState(System.currentTimeMillis())) { writeActivityState(); } } private void readActivityState() { try { activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME); } catch (Exception e) { logger.error("Failed to read %s file (%s)", ACTIVITY_STATE_NAME, e.getMessage()); activityState = null; } } private void readAttribution() { try { attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME); } catch (Exception e) { logger.error("Failed to read %s file (%s)", ATTRIBUTION_NAME, e.getMessage()); attribution = null; } } private synchronized 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; } private boolean checkActivityState(ActivityState activityState) { if (activityState == null) { logger.error("Missing activity state."); return false; } return true; } private boolean paused() { return offline || !isEnabled(); } }