All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.segment.analytics.Analytics Maven / Gradle / Ivy

There is a newer version: 2.5.3
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Segment.io, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.segment.analytics;

import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.util.Map;

import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.CREATED;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.DESTROYED;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.PAUSED;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.RESUMED;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.SAVE_INSTANCE;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.STARTED;
import static com.segment.analytics.IntegrationManager.ActivityLifecyclePayload.Type.STOPPED;
import static com.segment.analytics.Logger.OWNER_MAIN;
import static com.segment.analytics.Logger.VERB_CREATE;
import static com.segment.analytics.Utils.checkMain;
import static com.segment.analytics.Utils.getResourceBooleanOrThrow;
import static com.segment.analytics.Utils.getResourceIntegerOrThrow;
import static com.segment.analytics.Utils.getResourceString;
import static com.segment.analytics.Utils.hasPermission;
import static com.segment.analytics.Utils.isNullOrEmpty;

/**
 * The entry point into the Analytics for Android SDK.
 * 

* The idea is simple: one pipeline for all your data. Segment is the single hub to collect, * translate and route your data with the flip of a switch. *

* Analytics for Android will automatically batch events, queue them to disk, and upload it * periodically to Segment for you. It will also look up your project's settings (that you've * configured in the web interface), specifically looking up settings for bundled integrations, and * then initialize them for you on the user's phone, and mapping our standardized events to formats * they can all understand. You only need to instrument Segment once, then flip a switch to install * new tools. *

* This class is the main entry point into the client API. Use {@link * #with(android.content.Context)} for the global singleton instance or construct your own instance * with {@link Builder}. * * @see Segment.io */ public class Analytics { // Resource identifiers to define options in xml static final String WRITE_KEY_RESOURCE_IDENTIFIER = "analytics_write_key"; static final String QUEUE_SIZE_RESOURCE_IDENTIFIER = "analytics_queue_size"; static final String FLUSH_INTERVAL_IDENTIFIER = "analytics_flush_interval"; static final String DEBUGGING_RESOURCE_IDENTIFIER = "analytics_debugging"; static final Properties EMPTY_PROPERTIES = new Properties(); static final Handler HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { default: throw new AssertionError("Unknown handler message received: " + msg.what); } } }; static Analytics singleton = null; final Application application; final IntegrationManager integrationManager; final Stats stats; final TraitsCache traitsCache; final AnalyticsContext analyticsContext; final Options defaultOptions; final Logger logger; final boolean debuggingEnabled; boolean shutdown; Analytics(Application application, IntegrationManager integrationManager, Stats stats, TraitsCache traitsCache, AnalyticsContext analyticsContext, Options defaultOptions, Logger logger, boolean debuggingEnabled) { this.application = application; this.integrationManager = integrationManager; this.stats = stats; this.traitsCache = traitsCache; this.analyticsContext = analyticsContext; this.defaultOptions = defaultOptions; this.debuggingEnabled = debuggingEnabled; this.logger = logger; application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { submit(new ActivityLifecyclePayload(CREATED, activity, savedInstanceState)); } @Override public void onActivityStarted(Activity activity) { submit(new ActivityLifecyclePayload(STARTED, activity, null)); } @Override public void onActivityResumed(Activity activity) { submit(new ActivityLifecyclePayload(RESUMED, activity, null)); } @Override public void onActivityPaused(Activity activity) { submit(new ActivityLifecyclePayload(PAUSED, activity, null)); } @Override public void onActivityStopped(Activity activity) { submit(new ActivityLifecyclePayload(STOPPED, activity, null)); } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { submit(new ActivityLifecyclePayload(SAVE_INSTANCE, activity, outState)); } @Override public void onActivityDestroyed(Activity activity) { submit(new ActivityLifecyclePayload(DESTROYED, activity, null)); } }); } /** * The global default {@link Analytics} instance. *

* This instance is automatically initialized with defaults that are suitable to most * implementations. *

* If these settings do not meet the requirements of your application, you can override defaults * in {@code analytics.xml}, or you can construct your own instance with full control over the * configuration by using {@link Builder}. *

* By default, events are uploaded every 30 seconds, or every 20 events (whichever occurs first), * and debugging is disabled. */ public static Analytics with(Context context) { if (singleton == null) { if (context == null) { throw new IllegalArgumentException("Context must not be null."); } synchronized (Analytics.class) { if (singleton == null) { String writeKey = getResourceString(context, WRITE_KEY_RESOURCE_IDENTIFIER); Builder builder = new Builder(context, writeKey); try { // We need the exception to be able to tell if this was not defined, or if it was // incorrectly defined - something we shouldn't ignore int queueSize = getResourceIntegerOrThrow(context, QUEUE_SIZE_RESOURCE_IDENTIFIER); if (queueSize <= 0) { throw new IllegalStateException(QUEUE_SIZE_RESOURCE_IDENTIFIER + "(" + queueSize + ") may not be zero or negative."); } builder.queueSize(queueSize); } catch (Resources.NotFoundException e) { // when queueSize is not defined in xml, we'll use a default option in the builder } try { // We need the exception to be able to tell if this was not defined, or if it was // incorrectly defined - something we shouldn't ignore int flushInterval = getResourceIntegerOrThrow(context, FLUSH_INTERVAL_IDENTIFIER); if (flushInterval < 1) { throw new IllegalStateException(FLUSH_INTERVAL_IDENTIFIER + "(" + flushInterval + ") may not be zero or negative."); } builder.flushInterval(flushInterval); } catch (Resources.NotFoundException e) { // when flushInterval is not defined in xml, we'll use a default option in the builder } try { boolean debugging = getResourceBooleanOrThrow(context, DEBUGGING_RESOURCE_IDENTIFIER); builder.debugging(debugging); } catch (Resources.NotFoundException notFoundException) { // when debugging is not defined in xml, we'll try to figure it out from package flags String packageName = context.getPackageName(); try { final int flags = context.getPackageManager().getApplicationInfo(packageName, 0).flags; boolean debugging = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; builder.debugging(debugging); } catch (PackageManager.NameNotFoundException nameNotFoundException) { // if we still can't figure it out, we'll use the default options in the builder } } singleton = builder.build(); } } } return singleton; } /** * Set the global instance returned from {@link #with}. *

* This method must be called before any calls to {@link #with} and may only be called once. * * @since 2.3 */ public static void setSingletonInstance(Analytics analytics) { synchronized (Analytics.class) { if (singleton != null) { throw new IllegalStateException("Singleton instance already exists."); } singleton = analytics; } } /** * Returns {@code true} if debugging is enabled. * * @deprecated Use {@link #isDebugging()} instead. */ public boolean isLogging() { return debuggingEnabled; } /** * Returns {@code true} if debugging is enabled. * * @since 2.3 */ public boolean isDebugging() { return debuggingEnabled; } /** @see #identify(String, Traits, Options) */ public void identify(String userId) { identify(userId, null, null); } /** @see #identify(String, Traits, Options) */ public void identify(Traits traits) { identify(null, traits, null); } /** * Identify lets you tie one of your users and their actions to a recognizable {@code userId}. It * also lets you record {@code traits} about the user, like their email, name, account type, etc. *

* Traits and userId will be automatically cached and available on future sessions for the same * user. To update a trait on the server, simply call identify with the same user id (or null). * You can also use {@link #identify(Traits)} for this purpose. * * @param userId Unique identifier which you recognize a user by in your own database. If this is * null or empty, any previous id we have (could be the anonymous id) will be * used. * @param traits Traits about the user * @param options To configure the call * @throws IllegalArgumentException if userId is null or an empty string * @see Identify Documentation */ public void identify(String userId, Traits traits, Options options) { if (!isNullOrEmpty(userId)) { traitsCache.get().putUserId(userId); } if (options == null) { options = defaultOptions; } if (!isNullOrEmpty(traits)) { traitsCache.get().putAll(traits); traitsCache.save(); } BasePayload payload = new IdentifyPayload(traitsCache.get().anonymousId(), analyticsContext, traitsCache.get().userId(), traitsCache.get(), options); submit(payload); } /** @see #group(String, String, Traits, Options) */ public void group(String groupId) { group(null, groupId, null, null); } /** * The group method lets you associate a user with a group. It also lets you record custom traits * about the group, like industry or number of employees. *

* If you've called {@link #identify(String, Traits, Options)} before, this will automatically * remember the user id. If not, it will fall back to use the anonymousId instead. * * @param userId To match up a user with their associated group. * @param groupId Unique identifier which you recognize a group by in your own database. Must not * be null or empty. * @param options To configure the call * @throws IllegalArgumentException if groupId is null or an empty string * @see Group Documentation */ public void group(String userId, String groupId, Traits traits, Options options) { if (isNullOrEmpty(groupId)) { throw new IllegalArgumentException("groupId must not be null or empty."); } if (isNullOrEmpty(userId)) { userId = traitsCache.get().userId(); } if (!isNullOrEmpty(traits)) { traitsCache.get().putAll(traits); traitsCache.save(); } if (options == null) { options = defaultOptions; } BasePayload payload = new GroupPayload(traitsCache.get().anonymousId(), analyticsContext, userId, groupId, traitsCache.get(), options); submit(payload); } /** @see #track(String, Properties, Options) */ public void track(String event) { track(event, null, null); } /** @see #track(String, Properties, Options) */ public void track(String event, Properties properties) { track(event, properties, null); } /** * The track method is how you record any actions your users perform. Each action is known by a * name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions. * For * example a 'Purchased a Shirt' event might have properties like revenue or size. * * @param event Name of the event. Must not be null or empty. * @param properties {@link Properties} to add extra information to this call * @param options To configure the call * @throws IllegalArgumentException if event name is null or an empty string * @see Track Documentation */ public void track(String event, Properties properties, Options options) { if (isNullOrEmpty(event)) { throw new IllegalArgumentException("event must not be null or empty."); } if (properties == null) { properties = EMPTY_PROPERTIES; } if (options == null) { options = defaultOptions; } BasePayload payload = new TrackPayload(traitsCache.get().anonymousId(), analyticsContext, traitsCache.get().userId(), event, properties, options); submit(payload); } /** @see #screen(String, String, Properties, Options) */ public void screen(String category, String name) { screen(category, name, null, null); } /** @see #screen(String, String, Properties, Options) */ public void screen(String category, String name, Properties properties) { screen(category, name, properties, null); } /** * The screen methods let your record whenever a user sees a screen of your mobile app, and * attach * a name, category or properties to the screen. *

* Either category or name must be provided. * * @param category A category to describe the screen * @param name A name for the screen * @param properties {@link Properties} to add extra information to this call * @param options To configure the call * @see Screen Documentation */ public void screen(String category, String name, Properties properties, Options options) { if (isNullOrEmpty(category) && isNullOrEmpty(name)) { throw new IllegalArgumentException("either category or name must be provided."); } if (properties == null) { properties = EMPTY_PROPERTIES; } if (options == null) { options = defaultOptions; } BasePayload payload = new ScreenPayload(traitsCache.get().anonymousId(), analyticsContext, traitsCache.get().userId(), category, name, properties, options); submit(payload); } /** @see #alias(String, String, Options) */ public void alias(String previousId, String newId) { alias(previousId, newId, null); } /** * The alias method is used to merge two user identities, effectively connecting two sets of user * data as one. This is an advanced method, but it is required to manage user identities * successfully in some of our integrations. You should still call {@link #identify(String, * Traits, Options)} with {@code newId} if you want to use it as the default id. * * @param previousId The old id we want to map. If it is null, the userId we've cached will * automatically used. * @param newId The newId to map the old id to. Must not be null to empty. * @param options To configure the call * @throws IllegalArgumentException if newId is null or empty * @see Alias Documentation */ public void alias(String previousId, String newId, Options options) { if (isNullOrEmpty(newId)) { throw new IllegalArgumentException("newId must not be null or empty."); } if (isNullOrEmpty(previousId)) { previousId = traitsCache.get().userId(); } if (options == null) { options = defaultOptions; } BasePayload payload = new AliasPayload(traitsCache.get().anonymousId(), analyticsContext, newId, previousId, options); submit(payload); } /** * Flushes all messages in the queue to the server, and tell integrations to do the same. Note * that wil do nothing for bundled integrations that don't provide an explicit flush method. */ public void flush() { integrationManager.flush(); } /** Get the {@link AnalyticsContext} used by this instance. */ public AnalyticsContext getAnalyticsContext() { return analyticsContext; } /** Creates a {@link StatsSnapshot} of the current stats for this instance. */ public StatsSnapshot getSnapshot() { return stats.createSnapshot(); } /** Clear any information, including traits and user id about the current user. */ public void logout() { traitsCache.delete(application); analyticsContext.putTraits(traitsCache.get()); } /** Stops this instance from accepting further requests. */ public void shutdown() { if (this == singleton) { throw new UnsupportedOperationException("Default singleton instance cannot be shutdown."); } if (shutdown) { return; } integrationManager.shutdown(); stats.shutdown(); shutdown = true; } /** * Register to be notified when a bundled integration is ready. See {@link * OnIntegrationReadyListener} for more information. *

* This method must be called from the main thread. * * @since 2.1 * @deprecated Use {@link #registerOnIntegrationReady(OnIntegrationReadyListener)} instead. */ public void onIntegrationReady(OnIntegrationReadyListener onIntegrationReadyListener) { registerOnIntegrationReady(onIntegrationReadyListener); } /** * Register to be notified when a bundled integration is ready. See {@link * OnIntegrationReadyListener} for more information. *

* This method must be called from the main thread. *

* {@code * analytics.registerOnIntegrationReady(new OnIntegrationReadyListener() { * * \@Override public void onIntegrationReady(String key, Object integration) { * if("Mixpanel".equals(key)) { * ((MixpanelAPI) integration).clearSuperProperties(); * } * } * }); * } * @since 2.3 */ public void registerOnIntegrationReady(OnIntegrationReadyListener onIntegrationReadyListener) { checkMain(); integrationManager.dispatchRegisterIntegrationInitializedListener(onIntegrationReadyListener); } void submit(BasePayload payload) { logger.debug(OWNER_MAIN, VERB_CREATE, payload.id(), "type: %s", payload.type()); integrationManager.dispatchOperation(payload); } void submit(ActivityLifecyclePayload payload) { logger.debug(OWNER_MAIN, VERB_CREATE, payload.id(), "type: %s", payload.type); integrationManager.dispatchOperation(payload); } /** * A callback interface that is invoked when the Analytics client initializes bundled * integrations. *

* In most cases, integrations would have already been initialized, and the callback will be * invoked right away. The only time this not invoked immediately is when the application is * opened the very first time (right after a fresh install). */ public interface OnIntegrationReadyListener { /** * This method will be invoked once for each integration. The first argument is a * key to uniquely identify each integration (which will the same as the one in our public HTTP * API). The second argument will be the integration object itself, so you can call methods not * exposed as a part of our spec. This is useful if you're doing things like A/B testing. * * @param key A unique string to identify an integration. * @param integration The underlying instance that has been initialized with the settings from * Segment */ void onIntegrationReady(String key, Object integration); } /** Fluent API for creating {@link Analytics} instances. */ public static class Builder { private final Application application; private String writeKey; private String tag; private int queueSize = 20; private int flushInterval = 30; private Options defaultOptions; private boolean debuggingEnabled = false; /** Start building a new {@link Analytics} instance. */ public Builder(Context context, String writeKey) { if (context == null) { throw new IllegalArgumentException("Context must not be null."); } if (!hasPermission(context, Manifest.permission.INTERNET)) { throw new IllegalArgumentException("INTERNET permission is required."); } if (isNullOrEmpty(writeKey)) { throw new IllegalArgumentException("writeKey must not be null or empty."); } application = (Application) context.getApplicationContext(); this.writeKey = writeKey; } /** * Set the queue size at which the client should flush events. * * @deprecated Use {@link #queueSize(int)} instead. */ public Builder maxQueueSize(int maxQueueSize) { return queueSize(maxQueueSize); } /** * Set the queue size at which the client should flush events. * The client will automatically flush events every {@code flushInterval} seconds, or when the * queue reaches {@code queueSize}, whichever occurs first. */ public Builder queueSize(int queueSize) { if (queueSize <= 0) { throw new IllegalArgumentException("queueSize must be greater than or equal to zero."); } this.queueSize = queueSize; return this; } /** * Set the interval (in seconds) at which the client should flush events. * The client will automatically flush events every {@code flushInterval} seconds, or when the * queue reaches {@code queueSize}, whichever occurs first. */ public Builder flushInterval(int flushInterval) { if (flushInterval < 1) { throw new IllegalArgumentException("flushInterval must be greater than or equal to 1."); } this.flushInterval = flushInterval; return this; } /** * Set some default options for all calls. This options should not contain a timestamp. You * won't be able to change the integrations specified in this options object. */ public Builder defaultOptions(Options defaultOptions) { if (defaultOptions == null) { throw new IllegalArgumentException("defaultOptions must not be null."); } if (defaultOptions.timestamp() != null) { throw new IllegalArgumentException("default option must not contain timestamp."); } if (this.defaultOptions != null) { throw new IllegalStateException("defaultOptions is already set."); } // Make a defensive copy this.defaultOptions = new Options(); for (Map.Entry entry : defaultOptions.integrations().entrySet()) { this.defaultOptions.setIntegration(entry.getKey(), entry.getValue()); } return this; } /** * Set a tag for this instance. The tag is used to generate keys for caching. By default the * writeKey is used, but you may want to specify an alternative one, if you want the instances * to share different caches. For example, without this tag, all instances with the same * writeKey, will share the same traits. By specifying a custom tag for each instance of the * client, the instances will have a different traits instance. */ public Builder tag(String tag) { if (isNullOrEmpty(tag)) { throw new IllegalArgumentException("tag must not be null or empty."); } if (this.tag != null) { throw new IllegalStateException("tag is already set."); } this.tag = tag; return this; } /** * Set whether debugging is enabled or not. * * @deprecated Use {@link #debugging(boolean)} instead. */ public Builder logging(boolean debuggingEnabled) { return debugging(debuggingEnabled); } /** * Set whether debugging is enabled or not. * * @since 2.3 */ public Builder debugging(boolean debuggingEnabled) { this.debuggingEnabled = debuggingEnabled; return this; } /** Create a {@link Analytics} client. */ public Analytics build() { if (defaultOptions == null) { defaultOptions = new Options(); } if (isNullOrEmpty(tag)) tag = writeKey; Logger logger = new Logger(debuggingEnabled); Stats stats = new Stats(); SegmentHTTPApi segmentHTTPApi = new SegmentHTTPApi(writeKey); IntegrationManager integrationManager = IntegrationManager.create(application, segmentHTTPApi, stats, queueSize, flushInterval, tag, logger, debuggingEnabled); TraitsCache traitsCache = new TraitsCache(application, tag); AnalyticsContext analyticsContext = new AnalyticsContext(application, traitsCache.get()); return new Analytics(application, integrationManager, stats, traitsCache, analyticsContext, defaultOptions, logger, debuggingEnabled); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy