
com.segment.analytics.IntegrationManager Maven / Gradle / Ivy
package com.segment.analytics;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.segment.analytics.Logger.OWNER_INTEGRATION_MANAGER;
import static com.segment.analytics.Logger.VERB_DISPATCHED;
import static com.segment.analytics.Logger.VERB_DISPATCHING;
import static com.segment.analytics.Logger.VERB_INITIALIZED;
import static com.segment.analytics.Logger.VERB_INITIALIZING;
import static com.segment.analytics.Logger.VERB_SKIPPED;
import static com.segment.analytics.Utils.isConnected;
import static com.segment.analytics.Utils.isNullOrEmpty;
import static com.segment.analytics.Utils.panic;
import static com.segment.analytics.Utils.quitThread;
/**
* Manages bundled integrations. This class will maintain it's own queue for events to account for
* the latency between receiving the first event, fetching remote settings and enabling the
* integrations. Once we enable all integrations - we'll replay any events in the queue. This
* should
* only affect the first app install, subsequent launches will be use a cached value from disk.
*/
class IntegrationManager {
private static final String PROJECT_SETTINGS_CACHE_KEY = "project-settings";
static final int REQUEST_FETCH_SETTINGS = 1;
static final int REQUEST_INIT = 2;
static final int REQUEST_LIFECYCLE_EVENT = 3;
static final int REQUEST_ANALYTICS_EVENT = 4;
static final int REQUEST_FLUSH = 5;
private static final String INTEGRATION_MANAGER_THREAD_NAME =
Utils.THREAD_PREFIX + "IntegrationManager";
// A set of integrations available on the device
private final Set bundledIntegrations =
new HashSet();
// A map of integrations that were found on the device, so that we disable them for servers
private Map serverIntegrations = new LinkedHashMap();
private Queue operationQueue = new ArrayDeque();
final AtomicBoolean initialized = new AtomicBoolean();
static class ActivityLifecyclePayload {
enum Type {
CREATED, STARTED, RESUMED, PAUSED, STOPPED, SAVE_INSTANCE, DESTROYED
}
final Type type;
final WeakReference activityWeakReference;
final Bundle bundle;
final String id;
ActivityLifecyclePayload(Type type, Activity activity, Bundle bundle) {
this.type = type;
this.activityWeakReference = new WeakReference(activity);
this.bundle = bundle;
this.id = UUID.randomUUID().toString();
}
}
interface IntegrationOperation {
void run(AbstractIntegrationAdapter integration);
String id();
String type();
}
static class ActivityLifecycleOperation implements IntegrationOperation {
final ActivityLifecyclePayload payload;
final Logger logger;
ActivityLifecycleOperation(ActivityLifecyclePayload payload, Logger logger) {
this.payload = payload;
this.logger = logger;
}
@Override public void run(AbstractIntegrationAdapter integration) {
Activity activity = payload.activityWeakReference.get();
if (activity == null) {
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_SKIPPED, payload.id,
"type: " + payload.type);
}
return;
}
switch (payload.type) {
case CREATED:
integration.onActivityCreated(activity, payload.bundle);
break;
case STARTED:
break;
case RESUMED:
integration.onActivityResumed(activity);
break;
case PAUSED:
integration.onActivityPaused(activity);
break;
case STOPPED:
integration.onActivityStopped(activity);
break;
case SAVE_INSTANCE:
integration.onActivitySaveInstanceState(activity, payload.bundle);
break;
case DESTROYED:
integration.onActivityDestroyed(activity);
break;
default:
panic("Unknown payload type!" + payload.type);
}
}
@Override public String id() {
return payload.id;
}
@Override public String type() {
return payload.type.toString();
}
}
static class FlushOperation implements IntegrationOperation {
final String id;
FlushOperation() {
this.id = UUID.randomUUID().toString();
}
@Override public void run(AbstractIntegrationAdapter integration) {
integration.flush();
}
@Override public String id() {
return id;
}
@Override public String type() {
return "flush";
}
}
static class AnalyticsOperation implements IntegrationOperation {
final BasePayload payload;
AnalyticsOperation(BasePayload payload) {
this.payload = payload;
}
@Override public void run(AbstractIntegrationAdapter integration) {
if (!isBundledIntegrationEnabledForPayload(payload, integration)) return;
switch (payload.type()) {
case alias:
integration.alias((AliasPayload) payload);
break;
case group:
integration.group((GroupPayload) payload);
break;
case identify:
integration.identify((IdentifyPayload) payload);
break;
case screen:
integration.screen((ScreenPayload) payload);
break;
case track:
integration.track((TrackPayload) payload);
break;
default:
panic("Unknown payload type!" + payload.type());
}
}
@Override public String id() {
return payload.messageId();
}
@Override public String type() {
return payload.type().toString();
}
}
static IntegrationManager create(Context context, SegmentHTTPApi segmentHTTPApi, Stats stats,
Logger logger) {
StringCache projectSettingsCache =
new StringCache(Utils.getSharedPreferences(context), PROJECT_SETTINGS_CACHE_KEY);
return new IntegrationManager(context, segmentHTTPApi, projectSettingsCache, stats, logger);
}
final Context context;
final SegmentHTTPApi segmentHTTPApi;
final HandlerThread integrationManagerThread;
final Handler handler;
final Stats stats;
final Logger logger;
final StringCache projectSettingsCache;
private IntegrationManager(Context context, SegmentHTTPApi segmentHTTPApi,
StringCache projectSettingsCache, Stats stats, Logger logger) {
this.context = context;
this.segmentHTTPApi = segmentHTTPApi;
this.stats = stats;
this.logger = logger;
integrationManagerThread =
new HandlerThread(INTEGRATION_MANAGER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
integrationManagerThread.start();
handler = new IntegrationManagerHandler(integrationManagerThread.getLooper(), this);
loadIntegrations();
this.projectSettingsCache = projectSettingsCache;
ProjectSettings projectSettings = ProjectSettings.load(projectSettingsCache);
if (projectSettings == null) {
dispatchFetch();
} else {
dispatchInit(projectSettings);
// todo: stash staleness factor in a constant
if (projectSettings.timestamp() + 10800000L < System.currentTimeMillis()) {
dispatchFetch();
}
}
}
void loadIntegrations() {
initialized.set(false);
// Look up all the integrations available on the device. This is done early so that we can
// disable sending to these integrations from the server and properly fill the payloads.
addToBundledIntegrations(new AmplitudeIntegrationAdapter());
addToBundledIntegrations(new BugsnagIntegrationAdapter());
addToBundledIntegrations(new CountlyIntegrationAdapter());
addToBundledIntegrations(new CrittercismIntegrationAdapter());
addToBundledIntegrations(new FlurryIntegrationAdapter());
addToBundledIntegrations(new GoogleAnalyticsIntegrationAdapter());
addToBundledIntegrations(new LocalyticsIntegrationAdapter());
addToBundledIntegrations(new MixpanelIntegrationAdapter());
addToBundledIntegrations(new QuantcastIntegrationAdapter());
addToBundledIntegrations(new TapstreamIntegrationAdapter());
}
void addToBundledIntegrations(AbstractIntegrationAdapter abstractIntegrationAdapter) {
try {
Class.forName(abstractIntegrationAdapter.className());
bundledIntegrations.add(abstractIntegrationAdapter);
serverIntegrations.put(abstractIntegrationAdapter.key(), false);
} catch (ClassNotFoundException e) {
// ignored
}
}
void dispatchFetch() {
handler.sendMessage(handler.obtainMessage(REQUEST_FETCH_SETTINGS));
}
void performFetch() {
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_DISPATCHING, "fetch settings", null);
}
try {
if (isConnected(context)) {
ProjectSettings projectSettings = segmentHTTPApi.fetchSettings();
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_DISPATCHED, "fetch settings", null);
}
performInit(projectSettings);
} else {
// re-schedule in a minute, todo: move to constant, same as below
handler.sendMessageDelayed(handler.obtainMessage(REQUEST_FETCH_SETTINGS), 1000 * 60);
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_SKIPPED, "fetch settings", null);
}
}
} catch (IOException e) {
if (logger.loggingEnabled) {
logger.error(OWNER_INTEGRATION_MANAGER, VERB_DISPATCHING, "fetch settings", e, null);
}
// re-schedule in a minute, todo: move to constant
handler.sendMessageDelayed(handler.obtainMessage(REQUEST_FETCH_SETTINGS), 1000 * 60);
}
}
void dispatchInit(ProjectSettings projectSettings) {
handler.sendMessage(handler.obtainMessage(REQUEST_INIT, projectSettings));
}
void performInit(ProjectSettings projectSettings) {
String projectSettingsJson = projectSettings.toString();
projectSettingsCache.set(projectSettingsJson);
if (initialized.get()) return; // skip if already initialized, only cache this time
Iterator iterator = bundledIntegrations.iterator();
while (iterator.hasNext()) {
AbstractIntegrationAdapter integration = iterator.next();
if (projectSettings.containsKey(integration.key())) {
JsonMap settings = new JsonMap(projectSettings.getJsonMap(integration.key()));
try {
integration.initialize(context, settings);
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_INITIALIZED, integration.key(),
settings.toString());
}
} catch (InvalidConfigurationException e) {
iterator.remove();
if (logger.loggingEnabled) {
logger.error(OWNER_INTEGRATION_MANAGER, VERB_INITIALIZING, integration.key(), e,
settings.toString());
}
}
} else {
iterator.remove();
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_SKIPPED, integration.key(),
"not enabled in project settings: " + projectSettingsJson);
}
}
}
initialized.set(true);
replay();
}
void dispatch(ActivityLifecyclePayload payload) {
handler.sendMessage(handler.obtainMessage(REQUEST_LIFECYCLE_EVENT, payload));
}
void performEnqueue(ActivityLifecyclePayload payload) {
ActivityLifecycleOperation operation = new ActivityLifecycleOperation(payload, logger);
enqueue(operation);
}
void dispatch(BasePayload payload) {
handler.sendMessage(handler.obtainMessage(REQUEST_ANALYTICS_EVENT, payload));
}
void performEnqueue(BasePayload payload) {
enqueue(new AnalyticsOperation(payload));
}
void dispatchFlush() {
handler.sendMessage(handler.obtainMessage(REQUEST_FLUSH));
}
void performFlush() {
enqueue(new FlushOperation());
}
private void enqueue(IntegrationOperation operation) {
if (!initialized.get()) {
operationQueue.add(operation);
} else {
run(operation);
}
}
private void run(IntegrationOperation operation) {
for (AbstractIntegrationAdapter integration : bundledIntegrations) {
long startTime = System.currentTimeMillis();
operation.run(integration);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
if (logger.loggingEnabled) {
logger.debug(OWNER_INTEGRATION_MANAGER, VERB_DISPATCHED, operation.id(),
String.format("integration: %s, type: %s, duration: %s", integration.key(),
operation.type(), duration)
);
}
stats.dispatchIntegrationOperation(duration);
}
}
void replay() {
for (IntegrationOperation operation : operationQueue) {
run(operation);
}
operationQueue.clear();
operationQueue = null;
}
private static boolean isBundledIntegrationEnabledForPayload(BasePayload payload,
AbstractIntegrationAdapter integration) {
boolean enabled = true;
// look in the payload.context.integrations to see which Bundled integrations should be
// disabled. payload.integrations is reserved for the server, where all bundled integrations
// have been set to false
JsonMap integrations = payload.context().getIntegrations();
if (!isNullOrEmpty(integrations)) {
String key = integration.key();
if (integrations.containsKey(key)) {
enabled = integrations.getBoolean(key, true);
} else if (integrations.containsKey("All")) {
enabled = integrations.getBoolean("All", true);
} else if (integrations.containsKey("all")) {
enabled = integrations.getBoolean("all", true);
}
}
return enabled;
}
Map bundledIntegrations() {
return serverIntegrations;
}
private static class IntegrationManagerHandler extends Handler {
private final IntegrationManager integrationManager;
IntegrationManagerHandler(Looper looper, IntegrationManager integrationManager) {
super(looper);
this.integrationManager = integrationManager;
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_FETCH_SETTINGS:
integrationManager.performFetch();
break;
case REQUEST_INIT:
integrationManager.performInit((ProjectSettings) msg.obj);
break;
case REQUEST_LIFECYCLE_EVENT:
ActivityLifecyclePayload activityLifecyclePayload = (ActivityLifecyclePayload) msg.obj;
integrationManager.performEnqueue(activityLifecyclePayload);
break;
case REQUEST_ANALYTICS_EVENT:
BasePayload basePayload = (BasePayload) msg.obj;
integrationManager.performEnqueue(basePayload);
break;
case REQUEST_FLUSH:
integrationManager.performFlush();
break;
default:
Analytics.MAIN_LOOPER.post(new Runnable() {
@Override public void run() {
throw new AssertionError("Unhandled dispatcher message." + msg.what);
}
});
}
}
}
void shutdown() {
quitThread(integrationManagerThread);
if (operationQueue != null) {
operationQueue.clear();
operationQueue = null;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy