/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.util; import com.google.android.gms.analytics.GoogleAnalytics; import com.google.android.gms.analytics.HitBuilders; import com.google.android.gms.analytics.Tracker; import com.google.samples.apps.iosched.BuildConfig; import com.google.samples.apps.iosched.R; import com.google.samples.apps.iosched.settings.ConfMessageCardUtils; import com.google.samples.apps.iosched.settings.SettingsUtils; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; /** * Centralized Analytics interface to ensure proper initialization and * consistent analytics application across the app. * * For the purposes of this application, initialization of the Analytics tracker is broken * into two steps. {@link #prepareAnalytics(Context)} is called upon app creation, which sets up * a listener for changes to shared settings_prefs. When the user agrees to TOS, the listener triggers * the actual initialization step, setting up a Google Analytics tracker. This ensures that * no data is collected or accidentally sent before the TOS step, and that campaign tracking data * isn't accidentally deleted by starting and immediately disabling a tracker upon app creation. * */ public class AnalyticsHelper { private final static String TAG = LogUtils.makeLogTag(AnalyticsHelper.class); private static Context sAppContext = null; private static Tracker mTracker; /** Custom dimension slot number for the "attendee at venue" preference. * There's a finite number of custom dimensions, and they need to consistently be sent * in the same index in order to be tracked properly. For each custom dimension or metric, * always reserve an index. */ private static final int SLOT_ATTENDING_DIMENSION = 1; /** * The {@link PreferenceManager doesn't store a strong references to preference change * listeners. To prevent one from being garbage collected, a strong reference must be * created in app code. */ private static SharedPreferences.OnSharedPreferenceChangeListener sPrefListener; /** * Log a specific screen view under the {@code screenName} string. */ public static void sendScreenView(String screenName) { if (isInitialized()) { mTracker.setScreenName(screenName); mTracker.send(new HitBuilders.AppViewBuilder().build()); LOGD(TAG, "Screen View recorded: " + screenName); } } /** * Log a specific event under the {@code category}, {@code action}, and {@code label}. */ public static void sendEvent(String category, String action, String label, long value, HitBuilders.EventBuilder eventBuilder) { if(isInitialized()) { mTracker.send(eventBuilder .setCategory(category) .setAction(action) .setLabel(label) .setValue(value) .build()); LOGD(TAG, "Event recorded: \n" + "\tCategory: " + category + "\tAction: " + action + "\tLabel: " + label + "\tValue: " + value); } } /** * Log an specific event under the {@code category}, {@code action}, and {@code label}. */ public static void sendEvent(String category, String action, String label) { HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder(); sendEvent(category, action, label, 0, eventBuilder); } /** * Log an specific event under the {@code category}, {@code action}, and {@code label}. Attach * a custom dimension using the provided {@code dimensionIndex} and {@code dimensionValue} */ public static void sendEventWithCustomDimension(String category, String action, String label, int dimensionIndex, String dimensionValue) { // Create a new HitBuilder, populate it with the custom dimension, and send it along // to the rest of the event building process. HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder(); eventBuilder.setCustomDimension(dimensionIndex, dimensionValue); sendEvent(category, action, label, 0, eventBuilder); LOGD(TAG, "Custom Dimension Attached:\n" + "\tindex: " + dimensionIndex + "\tvalue: " + dimensionValue); } /** * Sets up Analytics to be initialized when the user agrees to TOS. If the user has already * done so (all runs of the app except the first run), initialize analytics Immediately. Note * that {@applicationContext} must be the Application level {@link Context} or this class will * leak the context. * * @param applicationContext The context that will later be used to initialize Analytics. */ public static void prepareAnalytics(Context applicationContext) { sAppContext = applicationContext; // The listener will initialize Analytics when the TOS is signed, or enable/disable // Analytics based on the "anonymous data collection" setting. setupPreferenceChangeListener(); // If TOS hasn't been signed yet, it's the first run. Exit. if (SettingsUtils.isTosAccepted(sAppContext)) { initializeAnalyticsTracker(sAppContext); } } /** * Initialize the analytics tracker in use by the application. This should only be called * once, when the TOS is signed. The {@code applicationContext} parameter MUST be the * application context or an object leak could occur. */ private static synchronized void initializeAnalyticsTracker(Context applicationContext) { sAppContext = applicationContext; if (mTracker == null) { int useProfile; if (BuildConfig.DEBUG) { LOGD(TAG, "Analytics manager using DEBUG ANALYTICS PROFILE."); useProfile = R.xml.analytics_debug; } else { useProfile = R.xml.analytics_release; } try { mTracker = GoogleAnalytics.getInstance(applicationContext).newTracker(useProfile); } catch (Exception e) { // If anything goes wrong, force an opt-out of tracking. It's better to accidentally // protect privacy than accidentally collect data. setAnalyticsEnabled(false); } } } /** * Listens for preference changes. When a preference change relevant to toggling Analytics * is detected, {@link AnalyticsHelper#enableOrDisableAnalyticsAsNecessary()} is called, which * will decide whether Analytics should be enabled or disabled based on settings_prefs and * application state. */ private static void setupPreferenceChangeListener() { SharedPreferences userPrefs = PreferenceManager.getDefaultSharedPreferences(sAppContext); sPrefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { // Most of the preferences will use these defaults. String category = "Preference"; if (key != null) { if (key.equals(SettingsUtils.PREF_TOS_ACCEPTED) || key.equals(SettingsUtils.PREF_ANALYTICS_ENABLED)) { // If TOS is accepted, initialize the Analytics Tracker. if (key.equals(SettingsUtils.PREF_TOS_ACCEPTED) && prefs.getBoolean(key, false) && mTracker == null) { initializeAnalyticsTracker(sAppContext); } // Technically it's possible to just look up the values in the pref // object provided and enable/disable in here, but it's safer to have all the // "should analytics run" logic collected in one place. enableOrDisableAnalyticsAsNecessary(); } else if (key.equals(SettingsUtils.PREF_LOCAL_TIMES)) { String label = "Local time"; // ANALYTICS EVENT: Updated "Show Local Times" setting. // Contains: The checkbox state of this setting. sendEvent(category, getAction(prefs, key), label); } else if (key.equals(BuildConfig.PREF_ATTENDEE_AT_VENUE)) { // Toggle the "Attending in person" custom dimension so we can track // how venue attendee behavior contrasts with remote attendee behavior. boolean attending = prefs.getBoolean(key, true); // ANALYTICS EVENT: Updated "On-Site Attendee" preference. // Contains: Whether the attendee is identifying themselves as onsite or remote. String attendeeType = attending ? "On-Site Attendee" : "Remote Attendee"; String label = "Will be at I/O"; sendEventWithCustomDimension(category, getAction(prefs, key), label, SLOT_ATTENDING_DIMENSION, attendeeType); } else if (key.equals(BuildConfig.PREF_CONF_MESSAGES_ENABLED)) { String label = "Conference Notification Cards"; // ANALYTICS EVENT: Updated "Conference Notification Cards" setting. // Contains: The checkbox state of this setting. sendEvent(category, getAction(prefs, key), label); } else if (key.equals(SettingsUtils.PREF_SYNC_CALENDAR)) { String label = "Sync with Google Calendar"; // ANALYTICS EVENT: Updated "Sync with Google Calendar" setting. // Contains: The checkbox state of this setting. sendEvent(category, getAction(prefs, key), label); } else if (key.equals(BuildConfig.PREF_SESSION_REMINDERS_ENABLED)) { String label = "Session Reminders"; // ANALYTICS EVENT: Updated "Session Reminders" setting. // Contains: The checkbox state of this setting. sendEvent(category, getAction(prefs, key), label); } else if (key.equals(BuildConfig.PREF_SESSION_FEEDBACK_REMINDERS_ENABLED)) { String label = "Feedback Reminders"; // ANALYTICS EVENT: Updated "Feedback Reminders" setting. // Contains: The checkbox state of this setting. sendEvent(category, getAction(prefs, key), label); } } } }; userPrefs.registerOnSharedPreferenceChangeListener(sPrefListener); } private static String getAction(SharedPreferences prefs, String key) { return prefs.getBoolean(key, true) ? "Checked" : "Unchecked"; } /** * Return the current initialization state which indicates whether events can be logged. */ private static boolean isInitialized() { // Google Analytics is initialized when this class has a reference to an app context and // an Analytics tracker has been created. return sAppContext != null // Is there an app context? && mTracker != null; // Is there a tracker? } /** * Performs the checks to determine if Analytics should be enabled. * @return whether or not it's safe to enable Analytics. */ private static boolean shouldEnableAnalytics() { // Analytics shouldn't run unless all the following are true: // 1) A tracker has been initialized in this class (as opposed to elsewhere in the app). // 2) The user has accepted TOS. // 3) "Anonymous usage data" is enabled in settings. return isInitialized() // Has Analytics been initialized? && SettingsUtils.isTosAccepted(sAppContext) // User has accepted TOS. && SettingsUtils.isAnalyticsEnabled(sAppContext); // Analytics enabled in settings. } /** * Checks application state and settings_prefs, then explicitly either enables or * disables the tracker. */ public static void enableOrDisableAnalyticsAsNecessary() { try { setAnalyticsEnabled(shouldEnableAnalytics()); LOGD(TAG, "Analytics" + (isInitialized() ? "" : " not") + " initialized" + ", TOS" + (SettingsUtils.isTosAccepted(sAppContext) ? "" : " not") + " accepted" + ", Setting is" + (SettingsUtils.isAnalyticsEnabled(sAppContext) ? "" : " not") + " checked"); } catch (Exception e) { setAnalyticsEnabled(false); } } /** * Enables or disables Analytics. * @param enableAnalytics Whether analytics should be enabled. */ private static void setAnalyticsEnabled(boolean enableAnalytics) { GoogleAnalytics instance = GoogleAnalytics.getInstance(sAppContext); if (instance != null) { instance.setAppOptOut(!enableAnalytics); LOGD(TAG, "Analytics enabled: " + enableAnalytics); } } }