content: /**
   * Copyright 2010-present Facebook.
   *
   * 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.*;
  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;
  
  
  /**
   * <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,
      }
  
      private enum SuppressionTimeoutBehavior {
          // Successfully logging an event will reset the timeout period (i.e., events will log no more than every N
          // seconds).
          RESET_TIMEOUT_WHEN_LOG_SUCCESSFUL,
          // Attempting to log an event, even if it is suppressed, will reset the timeout period (i.e., events will not
          // be logged until they have been "silent" for at least N seconds).
          RESET_TIMEOUT_WHEN_LOG_ATTEMPTED,
      }
  
      private static class EventSuppression {
          // Timeout period in seconds
          private int timeoutPeriod;
          private SuppressionTimeoutBehavior behavior;
  
          EventSuppression(int timeoutPeriod, SuppressionTimeoutBehavior behavior) {
              this.timeoutPeriod = timeoutPeriod;
              this.behavior = behavior;
          }
  
          int getTimeoutPeriod() {
              return timeoutPeriod;
          }
  
          SuppressionTimeoutBehavior getBehavior() {
              return behavior;
          }
      }
  
      // 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 APP_ACTIVATE_SUPPRESSION_PERIOD_IN_SECONDS            = 5 * 60;
  
      // Instance member variables
      private final Context context;
      private final AccessTokenAppIdPair accessTokenAppId;
  
      private static Map<AccessTokenAppIdPair, SessionEventsState> stateMap =
              new ConcurrentHashMap<AccessTokenAppIdPair, SessionEventsState>();
      private static Timer flushTimer;
      private static Timer supportsAttributionRecheckTimer;
      private static FlushBehavior flushBehavior = FlushBehavior.AUTO;
      private static boolean requestInFlight;
      private static Context applicationContext;
      private static Object staticLock = new Object();
      private static String hashedDeviceAndAppId;
      private static Map<String, Date> mapEventsToSuppressionTime = new HashMap<String, Date>();
      @SuppressWarnings("serial")
      private static Map<String, EventSuppression> mapEventNameToSuppress = new HashMap<String, EventSuppression>() {
          {
              put(AppEventsConstants.EVENT_NAME_ACTIVATED_APP,
                      new EventSuppression(APP_ACTIVATE_SUPPRESSION_PERIOD_IN_SECONDS,
                              SuppressionTimeoutBehavior.RESET_TIMEOUT_WHEN_LOG_ATTEMPTED));
          }
      };
  
      // 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) {
          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);
  
          AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
          logger.logEvent(AppEventsConstants.EVENT_NAME_ACTIVATED_APP);
      }
  
      /**
       * 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
      //
  
      private enum FlushReason {
          EXPLICIT,
          TIMER,
          SESSION_CHANGE,
          PERSISTED_EVENTS,
          EVENT_THRESHOLD,
          EAGER_FLUSHING_EVENT,
      }
  
      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 (session != null) {
              accessTokenAppId = new AccessTokenAppIdPair(session);
          } else {
              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 (flushTimer != null) {
                  return;
              }
              flushTimer = new Timer();
              supportsAttributionRecheckTimer = new Timer();
          }
  
          flushTimer.schedule(
                  new TimerTask() {
                      @Override
                      public void run() {
                          if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
                              flushAndWait(FlushReason.TIMER);
                          }
                      }
                  },
                  0,  // start immediately
                  FLUSH_PERIOD_IN_SECONDS * 1000);
  
          supportsAttributionRecheckTimer.schedule(
                  new TimerTask() {
                      @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);
                          }
                      }
                  },
                  0,   // start immediately
                  APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS * 1000);
      }
  
      private void logEvent(String eventName, Double valueToSum, Bundle parameters, boolean isImplicitlyLogged) {
  
          AppEvent event = new AppEvent(eventName, valueToSum, parameters, isImplicitlyLogged);
          logEvent(context, event, accessTokenAppId);
      }
  
      private static void logEvent(Context context, AppEvent event, AccessTokenAppIdPair accessTokenAppId) {
          if(shouldSuppressEvent(event)) {
              return;
          }
  
          SessionEventsState state = getSessionEventsState(context, accessTokenAppId);
          state.addEvent(event);
  
          flushIfNecessary();
      }
  
      // This will also update the timestamp based on specified behavior.
      private static boolean shouldSuppressEvent(AppEvent event) {
          EventSuppression suppressionInfo = mapEventNameToSuppress.get(event.getName());
          if (suppressionInfo == null) {
              return false;
          }
  
          Date timestamp = mapEventsToSuppressionTime.get(event.getName());
          boolean suppressed;
          if (timestamp == null) {
              suppressed = false;
          } else {
              long delta = new Date().getTime() - timestamp.getTime();
              suppressed = delta < (suppressionInfo.getTimeoutPeriod() * 1000);
          }
  
          // Update the time if we're not suppressed, OR if we are suppressed but the behavior is to reset even on
          // suppressed events.
          if (!suppressed ||
                  suppressionInfo.getBehavior() == SuppressionTimeoutBehavior.RESET_TIMEOUT_WHEN_LOG_ATTEMPTED) {
              mapEventsToSuppressionTime.put(event.getName(), new Date());
          }
  
          return suppressed;
      }
  
      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) {
          synchronized (staticLock) {
              SessionEventsState state = stateMap.get(accessTokenAppId);
              if (state == null) {
                  // Retrieve attributionId, but we will only send it if attribution is supported for the app.
                  AttributionIdentifiers attributionIdentifiers =
                      AttributionIdentifiers.getAttributionIdentifiers(context);
  
                  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);
              }
  
              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(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);
  
                  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());
          }
      }
  
      // 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);
          }
      }
  }
  