Android Open Source - Robook App Events Logger






From Project

Back to project page Robook.

License

The source code is released under:

MIT License

If you think the Android project Robook listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/**
 * Copyright 2010-present Facebook./* w w  w .java2s.c om*/
 *
 * 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.facebook;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import com.facebook.internal.AttributionIdentifiers;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import com.facebook.model.GraphObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


/**
 * <p>
 * The AppEventsLogger class allows the developer to log various types of events back to Facebook.  In order to log
 * events, the app must create an instance of this class via a {@link #newLogger newLogger} method, and then call
 * the various "log" methods off of that.
 * </p>
 * <p>
 * This client-side event logging is then available through Facebook App Insights
 * and for use with Facebook Ads conversion tracking and optimization.
 * </p>
 * <p>
 * The AppEventsLogger class has a few related roles:
 * <ul>
 * <li>
 * Logging predefined and application-defined events to Facebook App Insights with a
 * numeric value to sum across a large number of events, and an optional set of key/value
 * parameters that define "segments" for this event (e.g., 'purchaserStatus' : 'frequent', or
 * 'gamerLevel' : 'intermediate').  These events may also be used for ads conversion tracking,
 * optimization, and other ads related targeting in the future.
 * </li>
 * <li>
 * Methods that control the way in which events are flushed out to the Facebook servers.
 * </li>
 * </ul>
 * Here are some important characteristics of the logging mechanism provided by AppEventsLogger:
 * <ul>
 * <li>
 * Events are not sent immediately when logged.  They're cached and flushed out to the Facebook servers
 * in a number of situations:
 * <ul>
 * <li>when an event count threshold is passed (currently 100 logged events).</li>
 * <li>when a time threshold is passed (currently 60 seconds).</li>
 * <li>when an app has gone to background and is then brought back to the foreground.</li>
 * </ul>
 * <li>
 * Events will be accumulated when the app is in a disconnected state, and sent when the connection is
 * restored and one of the above 'flush' conditions are met.
 * </li>
 * <li>
 * The AppEventsLogger class is intended to be used from the thread it was created on.  Multiple AppEventsLoggers
 * may be created on other threads if desired.
 * </li>
 * <li>
 * The developer can call the setFlushBehavior method to force the flushing of events to only
 * occur on an explicit call to the `flush` method.
 * </li>
 * <li>
 * The developer can turn on console debug output for event logging and flushing to the server
 * Settings.addLoggingBehavior(LoggingBehavior.APP_EVENTS);
 * </li>
 * </ul>
 * Some things to note when logging events:
 * <ul>
 * <li>
 * There is a limit on the number of unique event names an app can use, on the order of 300.
 * </li>
 * <li>
 * There is a limit to the number of unique parameter names in the provided parameters that can
 * be used per event, on the order of 25.  This is not just for an individual call, but for all
 * invocations for that eventName.
 * </li>
 * <li>
 * Event names and parameter names (the keys in the NSDictionary) must be between 2 and 40 characters, and
 * must consist of alphanumeric characters, _, -, or spaces.
 * </li>
 * <li>
 * The length of each parameter value can be no more than on the order of 100 characters.
 * </li>
 * </ul>
 */
public class AppEventsLogger {
    // Enums

    /**
     * Controls when an AppEventsLogger sends log events to the server
     */
    public enum FlushBehavior {
        /**
         * Flush automatically: periodically (once a minute or after every 100 events), and always at app reactivation.
         * This is the default value.
         */
        AUTO,

        /**
         * Only flush when AppEventsLogger.flush() is explicitly invoked.
         */
        EXPLICIT_ONLY,
    }

    // Constants
    private static final String TAG = AppEventsLogger.class.getCanonicalName();

    private static final int NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER                  = 100;
    private static final int FLUSH_PERIOD_IN_SECONDS                               = 60;
    private static final int APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24;
    private static final int FLUSH_APP_SESSION_INFO_IN_SECONDS                     = 30;

    // Instance member variables
    private final Context context;
    private final AccessTokenAppIdPair accessTokenAppId;

    private static Map<AccessTokenAppIdPair, SessionEventsState> stateMap =
            new ConcurrentHashMap<AccessTokenAppIdPair, SessionEventsState>();
    private static ScheduledThreadPoolExecutor backgroundExecutor;
    private static FlushBehavior flushBehavior = FlushBehavior.AUTO;
    private static boolean requestInFlight;
    private static Context applicationContext;
    private static Object staticLock = new Object();
    private static String hashedDeviceAndAppId;

    // Rather than retaining Sessions, we extract the information we need and track app events by
    // application ID and access token (which may be null for Session-less calls). This avoids needing to
    // worry about Session lifecycle and also allows us to coalesce app events from different Sessions
    // that have the same access token/app ID.
    private static class AccessTokenAppIdPair implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String accessToken;
        private final String applicationId;

        AccessTokenAppIdPair(Session session) {
            this(session.getAccessToken(), session.getApplicationId());
        }

        AccessTokenAppIdPair(String accessToken, String applicationId) {
            this.accessToken = Utility.isNullOrEmpty(accessToken) ? null : accessToken;
            this.applicationId = applicationId;
        }

        String getAccessToken() {
            return accessToken;
        }

        String getApplicationId() {
            return applicationId;
        }

        @Override
        public int hashCode() {
            return (accessToken == null ? 0 : accessToken.hashCode()) ^
                    (applicationId == null ? 0 : applicationId.hashCode());
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof AccessTokenAppIdPair)) {
                return false;
            }
            AccessTokenAppIdPair p = (AccessTokenAppIdPair) o;
            return Utility.areObjectsEqual(p.accessToken, accessToken) &&
                    Utility.areObjectsEqual(p.applicationId, applicationId);
        }

        private static class SerializationProxyV1 implements Serializable {
            private static final long serialVersionUID = -2488473066578201069L;
            private final String accessToken;
            private final String appId;

            private SerializationProxyV1(String accessToken, String appId) {
                this.accessToken = accessToken;
                this.appId = appId;
            }

            private Object readResolve() {
                return new AccessTokenAppIdPair(accessToken, appId);
            }
        }

        private Object writeReplace() {
            return new SerializationProxyV1(accessToken, applicationId);
        }
    }

    /**
     * This method is deprecated.  Use {@link Settings#getLimitEventAndDataUsage(Context)} instead.
     */
    @Deprecated
    public static boolean getLimitEventUsage(Context context) {
        return Settings.getLimitEventAndDataUsage(context);
    }

    /**
     * This method is deprecated.  Use {@link Settings#setLimitEventAndDataUsage(Context, boolean)} instead.
     */
    @Deprecated
    public static void setLimitEventUsage(Context context, boolean limitEventUsage) {
        Settings.setLimitEventAndDataUsage(context, limitEventUsage);
    }

    /**
     * Notifies the events system that the app has launched & logs an activatedApp event.  Should be called whenever
     * your app becomes active, typically in the onResume() method of each long-running Activity of your app.
     *
     * Use this method if your application ID is stored in application metadata, otherwise see
     * {@link AppEventsLogger#activateApp(android.content.Context, String)}.
     *
     * @param context   Used to access the applicationId and the attributionId for non-authenticated users.
     */
    public static void activateApp(Context context) {
        Settings.sdkInitialize(context);
        activateApp(context, Utility.getMetadataApplicationId(context));
    }

    /**
     * Notifies the events system that the app has launched & logs an activatedApp event.  Should be called whenever
     * your app becomes active, typically in the onResume() method of each long-running Activity of your app.
     *
     * @param context   Used to access the attributionId for non-authenticated users.
     *
     * @param applicationId  The specific applicationId to report the activation for.
     */
    @SuppressWarnings("deprecation")
    public static void activateApp(Context context, String applicationId) {
        if (context == null || applicationId == null) {
            throw new IllegalArgumentException("Both context and applicationId must be non-null");
        }

        // activateApp supercedes publishInstall in the public API, so we need to explicitly invoke it, since the server
        // can't reliably infer install state for all conditions of an app activate.
        Settings.publishInstallAsync(context, applicationId, null);

        final AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
        final long eventTime = System.currentTimeMillis();
        backgroundExecutor.execute(new Runnable() {
          @Override
          public void run() {
            logger.logAppSessionResumeEvent(eventTime);
          }
        });
    }

    /**
     * Notifies the events system that the app has been deactivated (put in the background) and
     * tracks the application session information. Should be called whenever your app becomes
     * inactive, typically in the onPause() method of each long-running Activity of your app.
     *
     * Use this method if your application ID is stored in application metadata, otherwise see
     * {@link AppEventsLogger#deactivateApp(android.content.Context, String)}.
     *
     * @param context   Used to access the applicationId and the attributionId for non-authenticated users.
     */
    public static void deactivateApp(Context context) {
        deactivateApp(context, Utility.getMetadataApplicationId(context));
    }

    /**
     * Notifies the events system that the app has been deactivated (put in the background) and
     * tracks the application session information. Should be called whenever your app becomes
     * inactive, typically in the onPause() method of each long-running Activity of your app.
     *
     * @param context   Used to access the attributionId for non-authenticated users.
     *
     * @param applicationId  The specific applicationId to track session information for.
     */
    public static void deactivateApp(Context context, String applicationId) {
        if (context == null || applicationId == null) {
            throw new IllegalArgumentException("Both context and applicationId must be non-null");
        }

        final AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
        final long eventTime = System.currentTimeMillis();
        backgroundExecutor.execute(new Runnable() {
          @Override
          public void run() {
            logger.logAppSessionSuspendEvent(eventTime);
          }
        });
    }

    private void logAppSessionResumeEvent(long eventTime) {
        PersistedAppSessionInfo.onResume(applicationContext, accessTokenAppId, this, eventTime);
    }

    private void logAppSessionSuspendEvent(long eventTime) {
        PersistedAppSessionInfo.onSuspend(applicationContext, accessTokenAppId, this, eventTime);
    }

    /**
     * Build an AppEventsLogger instance to log events through.  The Facebook app that these events are targeted at
     * comes from this application's metadata. The application ID used to log events will be determined from
     * the app ID specified in the package metadata.
     *
     * @param context   Used to access the applicationId and the attributionId for non-authenticated users.
     *
     * @return          AppEventsLogger instance to invoke log* methods on.
     */
    public static AppEventsLogger newLogger(Context context) {
        return new AppEventsLogger(context, null, null);
    }

    /**
     * Build an AppEventsLogger instance to log events through.
     *
     * @param context        Used to access the attributionId for non-authenticated users.
     * @param session        Explicitly specified Session to log events against.  If null, the activeSession
     *                       will be used if it's open, otherwise the logging will happen against the default
     *                       app ID specified via the app ID specified in the package metadata.
     *
     * @return          AppEventsLogger instance to invoke log* methods on.
     */
    public static AppEventsLogger newLogger(Context context, Session session) {
        return new AppEventsLogger(context, null, session);
    }

    /**
     * Build an AppEventsLogger instance to log events through.
     *
     * @param context        Used to access the attributionId for non-authenticated users.
     * @param applicationId  Explicitly specified Facebook applicationId to log events against.  If null, the default
     *                       app ID specified in the package metadata will be used.
     * @param session        Explicitly specified Session to log events against.  If null, the activeSession
     *                       will be used if it's open, otherwise the logging will happen against the specified
     *                       app ID.
     *
     * @return          AppEventsLogger instance to invoke log* methods on.
     */
    public static AppEventsLogger newLogger(Context context, String applicationId, Session session) {
        return new AppEventsLogger(context, applicationId, session);
    }

    /**
     * Build an AppEventsLogger instance to log events that are attributed to the application but not to
     * any particular Session.
     *
     * @param context        Used to access the attributionId for non-authenticated users.
     * @param applicationId  Explicitly specified Facebook applicationId to log events against.  If null, the default
     *                       app ID specified
     *                       in the package metadata will be used.
     *
     * @return          AppEventsLogger instance to invoke log* methods on.
     */
    public static AppEventsLogger newLogger(Context context, String applicationId) {
        return new AppEventsLogger(context, applicationId, null);
    }

    /**
     * The action used to indicate that a flush of app events has occurred. This should
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
     * the {@link android.support.v4.content.LocalBroadcastManager}.
     */
    public static final String ACTION_APP_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_FLUSHED";

    public static final String APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_NUM_EVENTS_FLUSHED";
    public static final String APP_EVENTS_EXTRA_FLUSH_RESULT = "com.facebook.sdk.APP_EVENTS_FLUSH_RESULT";

    /**
     * Access the behavior that AppEventsLogger uses to determine when to flush logged events to the server. This
     * setting applies to all instances of AppEventsLogger.
     * @return specified flush behavior.
     */
    public static FlushBehavior getFlushBehavior() {
        synchronized (staticLock) {
            return flushBehavior;
        }
    }

    /**
     * Set the behavior that this AppEventsLogger uses to determine when to flush logged events to the server. This
     * setting applies to all instances of AppEventsLogger.
     * @param flushBehavior the desired behavior.
     */
    public static void setFlushBehavior(FlushBehavior flushBehavior) {
        synchronized (staticLock) {
            AppEventsLogger.flushBehavior = flushBehavior;
        }
    }

    /**
     * Log an app event with the specified name.
     * @param eventName eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
     *                  {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
     *                  constants are applicable.
     *                  Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
     *                  or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
     *                  have no more than ~300 distinct event names.
      */
    public void logEvent(String eventName) {
        logEvent(eventName, null);
    }

    /**
     * Log an app event with the specified name and the supplied value.
     * @param eventName eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
     *                  {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
     *                  constants are applicable.
     *                  Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
     *                  or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
     *                  have no more than ~300 distinct event names.
     *                  * @param eventName
     * @param valueToSum a value to associate with the event which will be summed up in Insights for across all
     *                   instances of the event, so that average values can be determined, etc.
     */
    public void logEvent(String eventName, double valueToSum) {
        logEvent(eventName, valueToSum, null);
    }

    /**
     * Log an app event with the specified name and set of parameters.
     * @param eventName eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
     *                  {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
     *                  constants are applicable.
     *                  Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
     *                  or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
     *                  have no more than ~300 distinct event names.
     * @param parameters A Bundle of parameters to log with the event.  Insights will allow looking at the logs of these
     *                   events via different parameter values.  You can log on the order of 10 parameters with each
     *                   distinct eventName.  It's advisable to keep the number of unique values provided for each
     *                   parameter in the, at most, thousands.  As an example, don't attempt to provide a unique
     *                   parameter value for each unique user in your app.  You won't get meaningful aggregate reporting
     *                   on so many parameter values.  The values in the bundles should be Strings or numeric values.
     */
    public void logEvent(String eventName, Bundle parameters) {
        logEvent(eventName, null, parameters, false);
    }

    /**
     * Log an app event with the specified name, supplied value, and set of parameters.
     * @param eventName eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
     *                  {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
     *                  constants are applicable.
     *                  Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
     *                  or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
     *                  have no more than ~300 distinct event names.
     * @param valueToSum a value to associate with the event which will be summed up in Insights for across all
     *                   instances of the event, so that average values can be determined, etc.
     * @param parameters A Bundle of parameters to log with the event.  Insights will allow looking at the logs of these
     *                   events via different parameter values.  You can log on the order of 10 parameters with each
     *                   distinct eventName.  It's advisable to keep the number of unique values provided for each
     *                   parameter in the, at most, thousands.  As an example, don't attempt to provide a unique
     *                   parameter value for each unique user in your app.  You won't get meaningful aggregate reporting
     *                   on so many parameter values.  The values in the bundles should be Strings or numeric values.
     */
    public void logEvent(String eventName, double valueToSum, Bundle parameters) {
        logEvent(eventName, valueToSum, parameters, false);
    }

    /**
     * Logs a purchase event with Facebook, in the specified amount and with the specified currency.
     *
     * @param purchaseAmount  Amount of purchase, in the currency specified by the 'currency' parameter. This value
     *                        will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
     * @param currency        Currency used to specify the amount.
     */
    public void logPurchase(BigDecimal purchaseAmount, Currency currency) {
        logPurchase(purchaseAmount, currency, null);
    }

    /**
     * Logs a purchase event with Facebook, in the specified amount and with the specified currency.  Additional
     * detail about the purchase can be passed in through the parameters bundle.
     *
     * @param purchaseAmount  Amount of purchase, in the currency specified by the 'currency' parameter. This value
     *                        will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
     * @param currency        Currency used to specify the amount.
     * @param parameters      Arbitrary additional information for describing this event.  Should have no more than
     *                        10 entries, and keys should be mostly consistent from one purchase event to the next.
     */
    public void logPurchase(BigDecimal purchaseAmount, Currency currency, Bundle parameters) {

        if (purchaseAmount == null) {
            notifyDeveloperError("purchaseAmount cannot be null");
            return;
        } else if (currency == null) {
            notifyDeveloperError("currency cannot be null");
            return;
        }

        if (parameters == null) {
            parameters = new Bundle();
        }
        parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.getCurrencyCode());

        logEvent(AppEventsConstants.EVENT_NAME_PURCHASED, purchaseAmount.doubleValue(), parameters);
        eagerFlush();
    }

    /**
     * Explicitly flush any stored events to the server.  Implicit flushes may happen depending on the value
     * of getFlushBehavior.  This method allows for explicit, app invoked flushing.
     */
    public void flush() {
        flush(FlushReason.EXPLICIT);
    }

    /**
     * Call this when the consuming Activity/Fragment receives an onStop() callback in order to persist any
     * outstanding events to disk, so they may be flushed at a later time. The next flush (explicit or not)
     * will check for any outstanding events and, if present, include them in that flush. Note that this call
     * may trigger an I/O operation on the calling thread. Explicit use of this method is not necessary
     * if the consumer is making use of {@link UiLifecycleHelper}, which will take care of making the call
     * in its own onStop() callback.
     */
    public static void onContextStop() {
        PersistedEvents.persistEvents(applicationContext, stateMap);
    }

    boolean isValidForSession(Session session) {
        AccessTokenAppIdPair other = new AccessTokenAppIdPair(session);
        return accessTokenAppId.equals(other);
    }

    /**
     * This method is intended only for internal use by the Facebook SDK and other use is unsupported.
     */
    public void logSdkEvent(String eventName, Double valueToSum, Bundle parameters) {
        logEvent(eventName, valueToSum, parameters, true);
    }

    /**
     * Returns the app ID this logger was configured to log to.
     * @return the Facebook app ID
     */
    public String getApplicationId() {
        return accessTokenAppId.getApplicationId();
    }

    //
    // Private implementation
    //

    @SuppressWarnings("UnusedDeclaration")
    private enum FlushReason {
        EXPLICIT,
        TIMER,
        SESSION_CHANGE,
        PERSISTED_EVENTS,
        EVENT_THRESHOLD,
        EAGER_FLUSHING_EVENT,
    }

    @SuppressWarnings("UnusedDeclaration")
    private enum FlushResult {
        SUCCESS,
        SERVER_ERROR,
        NO_CONNECTIVITY,
        UNKNOWN_ERROR
    }

    /**
     * Constructor is private, newLogger() methods should be used to build an instance.
     */
    private AppEventsLogger(Context context, String applicationId, Session session) {

        Validate.notNull(context, "context");
        this.context = context;

        if (session == null) {
            session = Session.getActiveSession();
        }

        // If we have a session and the appId passed is null or matches the session's app ID:
        if (session != null &&
            (applicationId == null || applicationId.equals(session.getApplicationId()))
          ) {
            accessTokenAppId = new AccessTokenAppIdPair(session);
        } else {
            // If no app ID passed, get it from the manifest:
            if (applicationId == null) {
                applicationId = Utility.getMetadataApplicationId(context);
            }
            accessTokenAppId = new AccessTokenAppIdPair(null, applicationId);
        }

        synchronized (staticLock) {

            if (hashedDeviceAndAppId == null) {
                hashedDeviceAndAppId = Utility.getHashedDeviceAndAppID(context, applicationId);
            }

            if (applicationContext == null) {
                applicationContext = context.getApplicationContext();
            }
        }

        initializeTimersIfNeeded();
    }

    private static void initializeTimersIfNeeded() {
        synchronized (staticLock) {
            if (backgroundExecutor != null) {
                return;
            }
            backgroundExecutor = new ScheduledThreadPoolExecutor(1);
        }

        final Runnable flushRunnable = new Runnable() {
            @Override
            public void run() {
                if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
                    flushAndWait(FlushReason.TIMER);
                }
            }
        };

        backgroundExecutor.scheduleAtFixedRate(
            flushRunnable,
            0,
            FLUSH_PERIOD_IN_SECONDS,
            TimeUnit.SECONDS
        );

        final Runnable attributionRecheckRunnable = new Runnable() {
            @Override
            public void run() {
                Set<String> applicationIds = new HashSet<String>();
                synchronized (staticLock) {
                    for (AccessTokenAppIdPair accessTokenAppId : stateMap.keySet()) {
                        applicationIds.add(accessTokenAppId.getApplicationId());
                    }
                }
                for (String applicationId : applicationIds) {
                    Utility.queryAppSettings(applicationId, true);
                }
            }
        };

        backgroundExecutor.scheduleAtFixedRate(
            attributionRecheckRunnable,
            0,
            APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS,
            TimeUnit.SECONDS
        );
    }

    private void logEvent(String eventName, Double valueToSum, Bundle parameters, boolean isImplicitlyLogged) {
        AppEvent event = new AppEvent(this.context, eventName, valueToSum, parameters, isImplicitlyLogged);
        logEvent(context, event, accessTokenAppId);
    }

   private static void logEvent(final Context context, final AppEvent event, final AccessTokenAppIdPair accessTokenAppId) {
        Settings.getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                SessionEventsState state = getSessionEventsState(context, accessTokenAppId);
                state.addEvent(event);
                flushIfNecessary();
            }
        });
    }

    static void eagerFlush() {
        if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
            flush(FlushReason.EAGER_FLUSHING_EVENT);
        }
    }

    private static void flushIfNecessary() {
        synchronized (staticLock) {
            if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
                if (getAccumulatedEventCount() > NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER) {
                    flush(FlushReason.EVENT_THRESHOLD);
                }
            }
        }
    }

    private static int getAccumulatedEventCount() {
        synchronized (staticLock) {

            int result = 0;
            for (SessionEventsState state : stateMap.values()) {
                result += state.getAccumulatedEventCount();
            }
            return result;
        }
    }

    // Creates a new SessionEventsState if not already in the map.
    private static SessionEventsState getSessionEventsState(Context context, AccessTokenAppIdPair accessTokenAppId) {
        // Do this work outside of the lock to prevent deadlocks in implementation of
        //  AdvertisingIdClient.getAdvertisingIdInfo, because that implementation blocks waiting on the main thread,
        //  which may also grab this staticLock.
        SessionEventsState state = stateMap.get(accessTokenAppId);
        AttributionIdentifiers attributionIdentifiers = null;
        if (state == null) {
            // Retrieve attributionId, but we will only send it if attribution is supported for the app.
            attributionIdentifiers = AttributionIdentifiers.getAttributionIdentifiers(context);
        }

        synchronized (staticLock) {
            // Check state again while we're locked.
            state = stateMap.get(accessTokenAppId);
            if (state == null) {
                state = new SessionEventsState(attributionIdentifiers, context.getPackageName(), hashedDeviceAndAppId);
                stateMap.put(accessTokenAppId, state);
            }
            return state;
        }
    }

    private static SessionEventsState getSessionEventsState(AccessTokenAppIdPair accessTokenAppId) {
        synchronized (staticLock) {
            return stateMap.get(accessTokenAppId);
        }
    }

    private static void flush(final FlushReason reason) {

        Settings.getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                flushAndWait(reason);
            }
        });
    }

    private static void flushAndWait(final FlushReason reason) {

        Set<AccessTokenAppIdPair> keysToFlush;
        synchronized (staticLock) {
            if (requestInFlight) {
                return;
            }
            requestInFlight = true;
            keysToFlush = new HashSet<AccessTokenAppIdPair>(stateMap.keySet());
        }

        accumulatePersistedEvents();

        FlushStatistics flushResults = null;
        try {
            flushResults = buildAndExecuteRequests(reason, keysToFlush);
        } catch (Exception e) {
            Log.d(TAG, "Caught unexpected exception while flushing: " + e.toString());
        }

        synchronized (staticLock) {
            requestInFlight = false;
        }

        if (flushResults != null) {
            final Intent intent = new Intent(ACTION_APP_EVENTS_FLUSHED);
            intent.putExtra(APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED, flushResults.numEvents);
            intent.putExtra(APP_EVENTS_EXTRA_FLUSH_RESULT, flushResults.result);
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent);
        }
    }

    private static FlushStatistics buildAndExecuteRequests(FlushReason reason, Set<AccessTokenAppIdPair> keysToFlush) {
        FlushStatistics flushResults = new FlushStatistics();

        boolean limitEventUsage = Settings.getLimitEventAndDataUsage(applicationContext);

        List<Request> requestsToExecute = new ArrayList<Request>();
        for (AccessTokenAppIdPair accessTokenAppId : keysToFlush) {
            SessionEventsState sessionEventsState = getSessionEventsState(accessTokenAppId);
            if (sessionEventsState == null) {
                continue;
            }

            Request request = buildRequestForSession(accessTokenAppId, sessionEventsState, limitEventUsage,
                    flushResults);
            if (request != null) {
                requestsToExecute.add(request);
            }
        }

        if (requestsToExecute.size() > 0) {
            Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Flushing %d events due to %s.",
                    flushResults.numEvents,
                    reason.toString());

            for (Request request : requestsToExecute) {
                // Execute the request synchronously. Callbacks will take care of handling errors and updating
                // our final overall result.
                request.executeAndWait();
            }
            return flushResults;
        }

        return null;
    }

    private static class FlushStatistics {
        public int numEvents = 0;
        public FlushResult result = FlushResult.SUCCESS;
    }

    private static Request buildRequestForSession(final AccessTokenAppIdPair accessTokenAppId,
            final SessionEventsState sessionEventsState, final boolean limitEventUsage,
            final FlushStatistics flushState) {
        String applicationId = accessTokenAppId.getApplicationId();

        Utility.FetchedAppSettings fetchedAppSettings = Utility.queryAppSettings(applicationId, false);

        final Request postRequest = Request.newPostRequest(
                null,
                String.format("%s/activities", applicationId),
                null,
                null);

        Bundle requestParameters = postRequest.getParameters();
        if (requestParameters == null) {
            requestParameters = new Bundle();
        }
        requestParameters.putString("access_token", accessTokenAppId.getAccessToken());
        postRequest.setParameters(requestParameters);

        int numEvents = sessionEventsState.populateRequest(postRequest, fetchedAppSettings.supportsImplicitLogging(),
                fetchedAppSettings.supportsAttribution(), limitEventUsage);
        if (numEvents == 0) {
            return null;
        }

        flushState.numEvents += numEvents;

        postRequest.setCallback(new Request.Callback() {
            @Override
            public void onCompleted(Response response) {
                handleResponse(accessTokenAppId, postRequest, response, sessionEventsState, flushState);
            }
        });

        return postRequest;
    }

    private static void handleResponse(AccessTokenAppIdPair accessTokenAppId, Request request, Response response,
            SessionEventsState sessionEventsState, FlushStatistics flushState) {
        FacebookRequestError error = response.getError();
        String resultDescription = "Success";

        FlushResult flushResult = FlushResult.SUCCESS;

        if (error != null) {
            final int NO_CONNECTIVITY_ERROR_CODE = -1;
            if (error.getErrorCode() == NO_CONNECTIVITY_ERROR_CODE) {
                resultDescription = "Failed: No Connectivity";
                flushResult = FlushResult.NO_CONNECTIVITY;
            } else {
                resultDescription = String.format("Failed:\n  Response: %s\n  Error %s",
                        response.toString(),
                        error.toString());
                flushResult = FlushResult.SERVER_ERROR;
            }
        }

        if (Settings.isLoggingBehaviorEnabled(LoggingBehavior.APP_EVENTS)) {
            String eventsJsonString = (String) request.getTag();
            String prettyPrintedEvents;

            try {
                JSONArray jsonArray = new JSONArray(eventsJsonString);
                prettyPrintedEvents = jsonArray.toString(2);
            } catch (JSONException exc) {
                prettyPrintedEvents = "<Can't encode events for debug logging>";
            }

            Logger.log(LoggingBehavior.APP_EVENTS, TAG,
                    "Flush completed\nParams: %s\n  Result: %s\n  Events JSON: %s",
                    request.getGraphObject().toString(),
                    resultDescription,
                    prettyPrintedEvents);
        }

        sessionEventsState.clearInFlightAndStats(error != null);

        if (flushResult == FlushResult.NO_CONNECTIVITY) {
            // We may call this for multiple requests in a batch, which is slightly inefficient since in principle
            // we could call it once for all failed requests, but the impact is likely to be minimal.
            // We don't call this for other server errors, because if an event failed because it was malformed, etc.,
            // continually retrying it will cause subsequent events to not be logged either.
            PersistedEvents.persistEvents(applicationContext, accessTokenAppId, sessionEventsState);
        }

        if (flushResult != FlushResult.SUCCESS) {
            // We assume that connectivity issues are more significant to report than server issues.
            if (flushState.result != FlushResult.NO_CONNECTIVITY) {
                flushState.result = flushResult;
            }
        }
    }

    private static int accumulatePersistedEvents() {
        PersistedEvents persistedEvents = PersistedEvents.readAndClearStore(applicationContext);

        int result = 0;
        for(AccessTokenAppIdPair accessTokenAppId : persistedEvents.keySet()) {
            SessionEventsState sessionEventsState = getSessionEventsState(applicationContext, accessTokenAppId);

            List<AppEvent> events = persistedEvents.getEvents(accessTokenAppId);
            sessionEventsState.accumulatePersistedEvents(events);
            result += events.size();
        }

        return result;
    }

    /**
     * Invoke this method, rather than throwing an Exception, for situations where user/server input might reasonably
     * cause this to occur, and thus don't want an exception thrown at production time, but do want logging
     * notification.
     */
    private static void notifyDeveloperError(String message) {
        Logger.log(LoggingBehavior.DEVELOPER_ERRORS, "AppEvents", message);
    }


    //
    // Deprecated Stuff
    //


    static class SessionEventsState {
        private List<AppEvent> accumulatedEvents = new ArrayList<AppEvent>();
        private List<AppEvent> inFlightEvents = new ArrayList<AppEvent>();
        private int numSkippedEventsDueToFullBuffer;
        private AttributionIdentifiers attributionIdentifiers;
        private String packageName;
        private String hashedDeviceAndAppId;

        public static final String EVENT_COUNT_KEY = "event_count";
        public static final String ENCODED_EVENTS_KEY = "encoded_events";
        public static final String NUM_SKIPPED_KEY = "num_skipped";

        private final int MAX_ACCUMULATED_LOG_EVENTS = 1000;

        public SessionEventsState(AttributionIdentifiers identifiers, String packageName, String hashedDeviceAndAppId) {
            this.attributionIdentifiers = identifiers;
            this.packageName = packageName;
            this.hashedDeviceAndAppId = hashedDeviceAndAppId;
        }

        // Synchronize here and in other methods on this class, because could be coming in from different
        // AppEventsLoggers on different threads pointing at the same session.
        public synchronized void addEvent(AppEvent event) {
            if (accumulatedEvents.size() + inFlightEvents.size() >= MAX_ACCUMULATED_LOG_EVENTS) {
                numSkippedEventsDueToFullBuffer++;
            } else {
                accumulatedEvents.add(event);
            }
        }

        public synchronized int getAccumulatedEventCount() {
            return accumulatedEvents.size();
        }

        public synchronized void clearInFlightAndStats(boolean moveToAccumulated) {
            if (moveToAccumulated) {
                accumulatedEvents.addAll(inFlightEvents);
            }
            inFlightEvents.clear();
            numSkippedEventsDueToFullBuffer = 0;
        }

        public int populateRequest(Request request, boolean includeImplicitEvents,
                boolean includeAttribution, boolean limitEventUsage) {

            int numSkipped;
            JSONArray jsonArray;
            synchronized (this) {
                numSkipped = numSkippedEventsDueToFullBuffer;

                // move all accumulated events to inFlight.
                inFlightEvents.addAll(accumulatedEvents);
                accumulatedEvents.clear();

                jsonArray = new JSONArray();
                for (AppEvent event : inFlightEvents) {
                    if (includeImplicitEvents || !event.getIsImplicit()) {
                        jsonArray.put(event.getJSONObject());
                    }
                }

                if (jsonArray.length() == 0) {
                    return 0;
                }
            }

            populateRequest(request, numSkipped, jsonArray, includeAttribution, limitEventUsage);
            return jsonArray.length();
        }

        public synchronized List<AppEvent> getEventsToPersist() {
            // We will only persist accumulated events, not ones currently in-flight. This means if an in-flight
            // request fails, those requests will not be persisted and thus might be lost if the process terminates
            // while the flush is in progress.
            List<AppEvent> result = accumulatedEvents;
            accumulatedEvents = new ArrayList<AppEvent>();
            return result;
        }

        public synchronized void accumulatePersistedEvents(List<AppEvent> events) {
            // We won't skip events due to a full buffer, since we already accumulated them once and persisted
            // them. But they will count against the buffer size when further events are accumulated.
            accumulatedEvents.addAll(events);
        }

        private void populateRequest(Request request, int numSkipped, JSONArray events, boolean includeAttribution,
                boolean limitEventUsage) {
            GraphObject publishParams = GraphObject.Factory.create();
            publishParams.setProperty("event", "CUSTOM_APP_EVENTS");

            if (numSkippedEventsDueToFullBuffer > 0) {
                publishParams.setProperty("num_skipped_events", numSkipped);
            }

            if (includeAttribution) {
                Utility.setAppEventAttributionParameters(publishParams, attributionIdentifiers,
                        hashedDeviceAndAppId, limitEventUsage);
            }

            // The code to get all the Extended info is safe but just in case we can wrap the whole
            // call in its own try/catch block since some of the things it does might cause
            // unexpected exceptions on rooted/funky devices:
            try {
              Utility.setAppEventExtendedDeviceInfoParameters(publishParams, applicationContext);
            } catch (Exception e) {
              // Swallow
            }

            publishParams.setProperty("application_package_name", packageName);

            request.setGraphObject(publishParams);

            Bundle requestParameters = request.getParameters();
            if (requestParameters == null) {
                requestParameters = new Bundle();
            }

            String jsonString = events.toString();
            if (jsonString != null) {
                requestParameters.putByteArray("custom_events_file", getStringAsByteArray(jsonString));
                request.setTag(jsonString);
            }
            request.setParameters(requestParameters);
        }

        private byte[] getStringAsByteArray(String jsonString) {
            byte[] jsonUtf8 = null;
            try {
                jsonUtf8 = jsonString.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                // shouldn't happen, but just in case:
                Utility.logd("Encoding exception: ", e);
            }
            return jsonUtf8;
        }
    }

    static class AppEvent implements Serializable {
        private static final long serialVersionUID = 1L;

        private JSONObject jsonObject;
        private boolean isImplicit;
        private static final HashSet<String> validatedIdentifiers = new HashSet<String>();
        private String name;

        public AppEvent(
            Context context,
            String eventName,
            Double valueToSum,
            Bundle parameters,
            boolean isImplicitlyLogged
        ) {

            validateIdentifier(eventName);

            this.name = eventName;

            isImplicit = isImplicitlyLogged;
            jsonObject = new JSONObject();

            try {
                jsonObject.put("_eventName", eventName);
                jsonObject.put("_logTime", System.currentTimeMillis() / 1000);
                jsonObject.put("_ui", Utility.getActivityName(context));

                if (valueToSum != null) {
                    jsonObject.put("_valueToSum", valueToSum.doubleValue());
                }

                if (isImplicit) {
                    jsonObject.put("_implicitlyLogged", "1");
                }

                String appVersion = Settings.getAppVersion();
                if (appVersion != null) {
                    jsonObject.put("_appVersion", appVersion);
                }

                if (parameters != null) {
                    for (String key : parameters.keySet()) {

                        validateIdentifier(key);

                        Object value = parameters.get(key);
                        if (!(value instanceof String) && !(value instanceof Number)) {
                            throw new FacebookException(
                                    String.format("Parameter value '%s' for key '%s' should be a string or a numeric type.",
                                            value, key));
                        }

                        jsonObject.put(key, value.toString());
                    }
                }

                if (!isImplicit) {
                    Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
                            "Created app event '%s'", jsonObject.toString());
                }
            } catch (JSONException jsonException) {

                // If any of the above failed, just consider this an illegal event.
                Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
                        "JSON encoding for app event failed: '%s'", jsonException.toString());
                jsonObject = null;

            }
        }

        public String getName() {
            return name;
        }

        private AppEvent(String jsonString, boolean isImplicit) throws JSONException {
            jsonObject = new JSONObject(jsonString);
            this.isImplicit = isImplicit;
        }

        public boolean getIsImplicit() { return isImplicit; }

        public JSONObject getJSONObject() {
            return jsonObject;
        }

        // throw exception if not valid.
        private void validateIdentifier(String identifier) {

            // Identifier should be 40 chars or less, and only have 0-9A-Za-z, underscore, hyphen, and space (but no
            // hyphen or space in the first position).
            final String regex = "^[0-9a-zA-Z_]+[0-9a-zA-Z _-]*$";

            final int MAX_IDENTIFIER_LENGTH = 40;
            if (identifier == null || identifier.length() == 0 || identifier.length() > MAX_IDENTIFIER_LENGTH) {
                if (identifier == null) {
                    identifier = "<None Provided>";
                }
                throw new FacebookException(
                    String.format("Identifier '%s' must be less than %d characters", identifier, MAX_IDENTIFIER_LENGTH));
            }

            boolean alreadyValidated = false;
            synchronized (validatedIdentifiers) {
                alreadyValidated = validatedIdentifiers.contains(identifier);
            }

            if (!alreadyValidated) {
                if (identifier.matches(regex)) {
                    synchronized (validatedIdentifiers) {
                        validatedIdentifiers.add(identifier);
                    }
                } else {
                    throw new FacebookException(
                            String.format("Skipping event named '%s' due to illegal name - must be under 40 chars " +
                                          "and alphanumeric, _, - or space, and not start with a space or hyphen.",
                                          identifier));
                }
            }

        }

        private static class SerializationProxyV1 implements Serializable {
            private static final long serialVersionUID = -2488473066578201069L;
            private final String jsonString;
            private final boolean isImplicit;

            private SerializationProxyV1(String jsonString, boolean isImplicit) {
                this.jsonString = jsonString;
                this.isImplicit = isImplicit;
            }

            private Object readResolve() throws JSONException {
                return new AppEvent(jsonString, isImplicit);
            }
        }

        private Object writeReplace() {
            return new SerializationProxyV1(jsonObject.toString(), isImplicit);
        }

        @Override
        public String toString() {
            return String.format("\"%s\", implicit: %b, json: %s", jsonObject.optString("_eventName"),
                    isImplicit, jsonObject.toString());
        }
    }

    static class PersistedAppSessionInfo {
        private static final String PERSISTED_SESSION_INFO_FILENAME =
                "AppEventsLogger.persistedsessioninfo";

        private static final Object staticLock = new Object();
        private static boolean hasChanges = false;
        private static boolean isLoaded = false;
        private static Map<AccessTokenAppIdPair, FacebookTimeSpentData> appSessionInfoMap;

        private static final Runnable appSessionInfoFlushRunnable = new Runnable() {
            @Override
            public void run() {
                PersistedAppSessionInfo.saveAppSessionInformation(applicationContext);
            }
        };

        @SuppressWarnings("unchecked")
        private static void restoreAppSessionInformation(Context context) {
            ObjectInputStream ois = null;

            synchronized (staticLock) {
                if (!isLoaded) {
                    try {
                        ois =
                            new ObjectInputStream(
                                context.openFileInput(PERSISTED_SESSION_INFO_FILENAME));
                        appSessionInfoMap =
                            (HashMap<AccessTokenAppIdPair, FacebookTimeSpentData>) ois.readObject();
                        Logger.log(
                            LoggingBehavior.APP_EVENTS,
                            "AppEvents",
                            "App session info loaded");
                    } catch (FileNotFoundException fex) {
                    } catch (Exception e) {
                        Log.d(TAG, "Got unexpected exception: " + e.toString());
                    } finally {
                        Utility.closeQuietly(ois);
                        context.deleteFile(PERSISTED_SESSION_INFO_FILENAME);
                        if (appSessionInfoMap == null) {
                            appSessionInfoMap =
                                    new HashMap<AccessTokenAppIdPair, FacebookTimeSpentData>();
                        }
                        // Regardless of the outcome of the load, the session information cache
                        // is always deleted. Therefore, always treat the session information cache
                        // as loaded
                        isLoaded = true;
                        hasChanges = false;
                    }
                }
            }
        }

        static void saveAppSessionInformation(Context context) {
            ObjectOutputStream oos = null;

            synchronized (staticLock) {
                if (hasChanges) {
                    try {
                        oos = new ObjectOutputStream(
                                new BufferedOutputStream(
                                    context.openFileOutput(
                                        PERSISTED_SESSION_INFO_FILENAME,
                                        Context.MODE_PRIVATE)));
                        oos.writeObject(appSessionInfoMap);
                        hasChanges = false;
                        Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents", "App session info saved");
                    } catch (Exception e) {
                        Log.d(TAG, "Got unexpected exception: " + e.toString());
                    } finally {
                        Utility.closeQuietly(oos);
                    }
                }
            }
        }

        static void onResume(
          Context context,
          AccessTokenAppIdPair accessTokenAppId,
          AppEventsLogger logger,
          long eventTime
        ) {
          synchronized (staticLock) {
            FacebookTimeSpentData timeSpentData = getTimeSpentData(context, accessTokenAppId);
            timeSpentData.onResume(logger, eventTime);
            onTimeSpentDataUpdate();
          }
        }

        static void onSuspend(
          Context context,
          AccessTokenAppIdPair accessTokenAppId,
          AppEventsLogger logger,
          long eventTime
        ) {
          synchronized (staticLock) {
            FacebookTimeSpentData timeSpentData = getTimeSpentData(context, accessTokenAppId);
            timeSpentData.onSuspend(logger, eventTime);
            onTimeSpentDataUpdate();
          }
        }

        private static FacebookTimeSpentData getTimeSpentData(
            Context context,
            AccessTokenAppIdPair accessTokenAppId
        ) {
            restoreAppSessionInformation(context);
            FacebookTimeSpentData result = null;

            result = appSessionInfoMap.get(accessTokenAppId);
            if (result == null) {
                result = new FacebookTimeSpentData();
                appSessionInfoMap.put(accessTokenAppId, result);
            }

            return result;
        }

        private static void onTimeSpentDataUpdate() {
            if (!hasChanges) {
                hasChanges = true;
                backgroundExecutor.schedule(
                    appSessionInfoFlushRunnable,
                    FLUSH_APP_SESSION_INFO_IN_SECONDS,
                    TimeUnit.SECONDS);
            }
        }
    }

    // Read/write operations are thread-safe/atomic across all instances of PersistedEvents, but modifications
    // to any individual instance are not thread-safe.
    static class PersistedEvents {
        static final String PERSISTED_EVENTS_FILENAME = "AppEventsLogger.persistedevents";

        private static Object staticLock = new Object();

        private Context context;
        private HashMap<AccessTokenAppIdPair, List<AppEvent>> persistedEvents =
                new HashMap<AccessTokenAppIdPair, List<AppEvent>>();

        private PersistedEvents(Context context) {
            this.context = context;
        }

        public static PersistedEvents readAndClearStore(Context context) {
            synchronized (staticLock) {
                PersistedEvents persistedEvents = new PersistedEvents(context);

                persistedEvents.readAndClearStore();

                return persistedEvents;
            }
        }
        public static void persistEvents(Context context, AccessTokenAppIdPair accessTokenAppId,
                SessionEventsState eventsToPersist) {
            Map<AccessTokenAppIdPair, SessionEventsState> map = new HashMap<AccessTokenAppIdPair, SessionEventsState>();
            map.put(accessTokenAppId, eventsToPersist);
            persistEvents(context, map);
        }

        public static void persistEvents(Context context,
                Map<AccessTokenAppIdPair, SessionEventsState> eventsToPersist) {
            synchronized (staticLock) {
                // Note that we don't track which instance of AppEventsLogger added a particular event to
                // SessionEventsState; when a particular Context is being destroyed, we'll persist all accumulated
                // events. More sophisticated tracking could be done to try to reduce unnecessary persisting of events,
                // but the overall number of events is not expected to be large.
                PersistedEvents persistedEvents = readAndClearStore(context);

                for (Map.Entry<AccessTokenAppIdPair, SessionEventsState> entry : eventsToPersist.entrySet()) {
                    List<AppEvent> events = entry.getValue().getEventsToPersist();
                    if (events.size() == 0) {
                        continue;
                    }

                    persistedEvents.addEvents(entry.getKey(), events);
                }

                persistedEvents.write();
            }
        }

        public Set<AccessTokenAppIdPair> keySet() {
            return persistedEvents.keySet();
        }

        public List<AppEvent> getEvents(AccessTokenAppIdPair accessTokenAppId) {
            return persistedEvents.get(accessTokenAppId);
        }

        private void write() {
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(
                        new BufferedOutputStream(context.openFileOutput(PERSISTED_EVENTS_FILENAME, 0)));
                oos.writeObject(persistedEvents);
            } catch (Exception e) {
                Log.d(TAG, "Got unexpected exception: " + e.toString());
            } finally {
                Utility.closeQuietly(oos);
            }
        }

        private void readAndClearStore() {
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(
                        new BufferedInputStream(context.openFileInput(PERSISTED_EVENTS_FILENAME)));

                @SuppressWarnings("unchecked")
                HashMap<AccessTokenAppIdPair, List<AppEvent>> obj =
                        (HashMap<AccessTokenAppIdPair, List<AppEvent>>) ois.readObject();

                // Note: We delete the store before we store the events; this means we'd prefer to lose some
                // events in the case of exception rather than potentially log them twice.
                context.getFileStreamPath(PERSISTED_EVENTS_FILENAME).delete();
                persistedEvents = obj;
            } catch (FileNotFoundException e) {
                // Expected if we never persisted any events.
            } catch (Exception e) {
                Log.d(TAG, "Got unexpected exception: " + e.toString());
            } finally {
                Utility.closeQuietly(ois);
            }
        }

        public void addEvents(AccessTokenAppIdPair accessTokenAppId, List<AppEvent> eventsToPersist) {
            if (!persistedEvents.containsKey(accessTokenAppId)) {
                persistedEvents.put(accessTokenAppId, new ArrayList<AppEvent>());
            }
            persistedEvents.get(accessTokenAppId).addAll(eventsToPersist);
        }
    }
}




Java Source Code List

com.facebook.AccessTokenSource.java
com.facebook.AccessToken.java
com.facebook.AppEventsConstants.java
com.facebook.AppEventsLogger.java
com.facebook.AppLinkData.java
com.facebook.AuthorizationClient.java
com.facebook.BoltsMeasurementEventListener.java
com.facebook.FacebookAppLinkResolver.java
com.facebook.FacebookAuthorizationException.java
com.facebook.FacebookBroadcastReceiver.java
com.facebook.FacebookDialogException.java
com.facebook.FacebookException.java
com.facebook.FacebookGraphObjectException.java
com.facebook.FacebookOperationCanceledException.java
com.facebook.FacebookRequestError.java
com.facebook.FacebookSdkVersion.java
com.facebook.FacebookServiceException.java
com.facebook.FacebookTimeSpentData.java
com.facebook.GetTokenClient.java
com.facebook.HttpMethod.java
com.facebook.InsightsLogger.java
com.facebook.LegacyHelper.java
com.facebook.LoggingBehavior.java
com.facebook.LoginActivity.java
com.facebook.NativeAppCallAttachmentStore.java
com.facebook.NativeAppCallContentProvider.java
com.facebook.NonCachingTokenCachingStrategy.java
com.facebook.ProgressNoopOutputStream.java
com.facebook.ProgressOutputStream.java
com.facebook.RequestAsyncTask.java
com.facebook.RequestBatch.java
com.facebook.RequestOutputStream.java
com.facebook.RequestProgress.java
com.facebook.Request.java
com.facebook.Response.java
com.facebook.SessionDefaultAudience.java
com.facebook.SessionLoginBehavior.java
com.facebook.SessionState.java
com.facebook.Session.java
com.facebook.Settings.java
com.facebook.SharedPreferencesTokenCachingStrategy.java
com.facebook.TestSession.java
com.facebook.TokenCachingStrategy.java
com.facebook.UiLifecycleHelper.java
com.facebook.android.AsyncFacebookRunner.java
com.facebook.android.DialogError.java
com.facebook.android.FacebookError.java
com.facebook.android.Facebook.java
com.facebook.android.FbDialog.java
com.facebook.android.Util.java
com.facebook.internal.AnalyticsEvents.java
com.facebook.internal.AttributionIdentifiers.java
com.facebook.internal.CacheableRequestBatch.java
com.facebook.internal.FileLruCache.java
com.facebook.internal.ImageDownloader.java
com.facebook.internal.ImageRequest.java
com.facebook.internal.ImageResponseCache.java
com.facebook.internal.ImageResponse.java
com.facebook.internal.Logger.java
com.facebook.internal.NativeProtocol.java
com.facebook.internal.PlatformServiceClient.java
com.facebook.internal.ServerProtocol.java
com.facebook.internal.SessionAuthorizationType.java
com.facebook.internal.SessionTracker.java
com.facebook.internal.UrlRedirectCache.java
com.facebook.internal.Utility.java
com.facebook.internal.Validate.java
com.facebook.internal.WorkQueue.java
com.facebook.internal.package-info.java
com.facebook.model.CreateGraphObject.java
com.facebook.model.GraphLocation.java
com.facebook.model.GraphMultiResult.java
com.facebook.model.GraphObjectList.java
com.facebook.model.GraphObject.java
com.facebook.model.GraphPlace.java
com.facebook.model.GraphUser.java
com.facebook.model.JsonUtil.java
com.facebook.model.OpenGraphAction.java
com.facebook.model.OpenGraphObject.java
com.facebook.model.PropertyName.java
com.facebook.widget.FacebookDialog.java
com.facebook.widget.FacebookFragment.java
com.facebook.widget.FriendPickerFragment.java
com.facebook.widget.GraphObjectAdapter.java
com.facebook.widget.GraphObjectCursor.java
com.facebook.widget.GraphObjectPagingLoader.java
com.facebook.widget.LoginButton.java
com.facebook.widget.PickerFragment.java
com.facebook.widget.PlacePickerFragment.java
com.facebook.widget.ProfilePictureView.java
com.facebook.widget.SimpleGraphObjectCursor.java
com.facebook.widget.ToolTipPopup.java
com.facebook.widget.UserSettingsFragment.java
com.facebook.widget.WebDialog.java
unipg.dmi.robook.AbstractAdkActivity.java
unipg.dmi.robook.MainActivity.java
unipg.dmi.robook.Preview.java
unipg.dmi.robook.UpdaterService.java
unipg.dmi.robook.prefs.java