/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Facebook.
*
* As with any software that integrates with the Facebook platform, your use of
* this software is subject to the Facebook Developer Principles and Policies
* [http://developers.facebook.com/policy/]. This copyright notice shall be
* included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook.appevents;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import bolts.AppLinks;
import com.facebook.AccessToken;
import com.facebook.FacebookException;
import com.facebook.FacebookRequestError;
import com.facebook.FacebookSdk;
import com.facebook.GraphRequest;
import com.facebook.GraphResponse;
import com.facebook.LoggingBehavior;
import com.facebook.internal.AttributionIdentifiers;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
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:
* </p>
* <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>
* <p>
* 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 15 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 by
* calling FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS);
* </li>
* </ul>
* </p>
* <p>
* 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>
* </p>
*/
public class AppEventsLogger {
// Enums
/**
* Controls when an AppEventsLogger sends log events to the server
*/
public enum FlushBehavior {
/**
* Flush automatically: periodically (every 15 seconds 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 = 15;
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;
public static final String APP_EVENT_PREFERENCES = "com.facebook.sdk.appEventPreferences";
private static final String SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT =
"_fbSourceApplicationHasBeenSet";
// 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 anonymousAppDeviceGUID;
private static String sourceApplication;
private static boolean isOpenedByApplink;
private static class AccessTokenAppIdPair implements Serializable {
private static final long serialVersionUID = 1L;
private final String accessTokenString;
private final String applicationId;
AccessTokenAppIdPair(AccessToken accessToken) {
this(accessToken.getToken(), FacebookSdk.getApplicationId());
}
AccessTokenAppIdPair(String accessTokenString, String applicationId) {
this.accessTokenString = Utility.isNullOrEmpty(accessTokenString)
? null
: accessTokenString;
this.applicationId = applicationId;
}
String getAccessTokenString() {
return accessTokenString;
}
String getApplicationId() {
return applicationId;
}
@Override
public int hashCode() {
return (accessTokenString == null ? 0 : accessTokenString.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.accessTokenString, accessTokenString) &&
Utility.areObjectsEqual(p.applicationId, applicationId);
}
private static class SerializationProxyV1 implements Serializable {
private static final long serialVersionUID = -2488473066578201069L;
private final String accessTokenString;
private final String appId;
private SerializationProxyV1(String accessTokenString, String appId) {
this.accessTokenString = accessTokenString;
this.appId = appId;
}
private Object readResolve() {
return new AccessTokenAppIdPair(accessTokenString, appId);
}
}
private Object writeReplace() {
return new SerializationProxyV1(accessTokenString, applicationId);
}
}
/**
* 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.
* <p/>
* 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) {
FacebookSdk.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.
*/
public static void activateApp(Context context, String applicationId) {
if (context == null || applicationId == null) {
throw new IllegalArgumentException("Both context and applicationId must be non-null");
}
if ((context instanceof Activity)) {
setSourceApplication((Activity) context);
} else {
// If context is not an Activity, we cannot get intent nor calling activity.
resetSourceApplication();
Log.d(AppEventsLogger.class.getName(),
"To set source application the context of activateApp must be an instance of" +
" Activity");
}
// activateApp supersedes 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.
FacebookSdk.publishInstallAsync(context, applicationId);
final AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
final long eventTime = System.currentTimeMillis();
final String sourceApplicationInfo = getSourceApplication();
backgroundExecutor.execute(new Runnable() {
@Override
public void run() {
logger.logAppSessionResumeEvent(eventTime, sourceApplicationInfo);
}
});
}
/**
* 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");
}
resetSourceApplication();
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, String sourceApplicationInfo) {
PersistedAppSessionInfo.onResume(
applicationContext,
accessTokenAppId,
this,
eventTime,
sourceApplicationInfo);
}
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 accessToken Access token to use for logging events. If null, the active access token
* will be used, if any; if not the logging will happen against the default
* app ID specified in the package metadata.
*/
public static AppEventsLogger newLogger(Context context, AccessToken accessToken) {
return new AppEventsLogger(context, null, accessToken);
}
/**
* 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 accessToken Access token to use for logging events. If null, the active access token
* will be used, if any; if not the logging will happen against the default
* app ID specified in the package metadata.
* @return AppEventsLogger instance to invoke log* methods on.
*/
public static AppEventsLogger newLogger(
Context context,
String applicationId,
AccessToken accessToken) {
return new AppEventsLogger(context, applicationId, accessToken);
}
/**
* 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 must not 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 must not 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 must not 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
* limit the number of unique values provided for each parameter in the
* 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 must not 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
* limit the number of unique values provided for each parameter in the
* 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. This 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 necessary.
*/
public static void onContextStop() {
// TODO: (v4) add onContextStop() to samples that use the logger.
PersistedEvents.persistEvents(applicationContext, stateMap);
}
/**
* Determines if the logger is valid for the given access token.
* @param accessToken The access token to check.
* @return True if the access token is valid for this logger.
*/
public boolean isValidForAccessToken(AccessToken accessToken) {
AccessTokenAppIdPair other = new AccessTokenAppIdPair(accessToken);
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, AccessToken accessToken) {
Validate.notNull(context, "context");
this.context = context;
if (accessToken == null) {
accessToken = AccessToken.getCurrentAccessToken();
}
// If we have a session and the appId passed is null or matches the session's app ID:
if (accessToken != null &&
(applicationId == null || applicationId.equals(accessToken.getApplicationId()))
) {
accessTokenAppId = new AccessTokenAppIdPair(accessToken);
} 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 (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) {
FacebookSdk.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(),
getAnonymousAppDeviceGUID(context));
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) {
FacebookSdk.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) {
Utility.logd(TAG, "Caught unexpected exception while flushing: ", e);
}
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 = FacebookSdk.getLimitEventAndDataUsage(applicationContext);
List<GraphRequest> requestsToExecute = new ArrayList<GraphRequest>();
for (AccessTokenAppIdPair accessTokenAppId : keysToFlush) {
SessionEventsState sessionEventsState = getSessionEventsState(accessTokenAppId);
if (sessionEventsState == null) {
continue;
}
GraphRequest 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 (GraphRequest 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 GraphRequest 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 GraphRequest postRequest = GraphRequest.newPostRequest(
null,
String.format("%s/activities", applicationId),
null,
null);
Bundle requestParameters = postRequest.getParameters();
if (requestParameters == null) {
requestParameters = new Bundle();
}
requestParameters.putString("access_token", accessTokenAppId.getAccessTokenString());
postRequest.setParameters(requestParameters);
if (fetchedAppSettings == null) {
return null;
}
int numEvents = sessionEventsState.populateRequest(
postRequest,
fetchedAppSettings.supportsImplicitLogging(),
limitEventUsage);
if (numEvents == 0) {
return null;
}
flushState.numEvents += numEvents;
postRequest.setCallback(new GraphRequest.Callback() {
@Override
public void onCompleted(GraphResponse response) {
handleResponse(accessTokenAppId, postRequest, response, sessionEventsState, flushState);
}
});
return postRequest;
}
private static void handleResponse(
AccessTokenAppIdPair accessTokenAppId,
GraphRequest request,
GraphResponse 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 (FacebookSdk.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);
}
/**
* Source Application setters and getters
*/
private static void setSourceApplication(Activity activity) {
ComponentName callingApplication = activity.getCallingActivity();
if (callingApplication != null) {
String callingApplicationPackage = callingApplication.getPackageName();
if (callingApplicationPackage.equals(activity.getPackageName())) {
// open by own app.
resetSourceApplication();
return;
}
sourceApplication = callingApplicationPackage;
}
// Tap icon to open an app will still get the old intent if the activity was opened by an
// intent before. Introduce an extra field in the intent to force clear the
// sourceApplication.
Intent openIntent = activity.getIntent();
if (openIntent == null ||
openIntent.getBooleanExtra(SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT, false)) {
resetSourceApplication();
return;
}
Bundle applinkData = AppLinks.getAppLinkData(openIntent);
if (applinkData == null) {
resetSourceApplication();
return;
}
isOpenedByApplink = true;
Bundle applinkReferrerData = applinkData.getBundle("referer_app_link");
if (applinkReferrerData == null) {
sourceApplication = null;
return;
}
String applinkReferrerPackage = applinkReferrerData.getString("package");
sourceApplication = applinkReferrerPackage;
// Mark this intent has been used to avoid use this intent again and again.
openIntent.putExtra(SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT, true);
return;
}
static void setSourceApplication(String applicationPackage, boolean openByAppLink) {
sourceApplication = applicationPackage;
isOpenedByApplink = openByAppLink;
}
static String getSourceApplication() {
String openType = "Unclassified";
if (isOpenedByApplink) {
openType = "Applink";
}
if (sourceApplication != null) {
return openType + "(" + sourceApplication + ")";
}
return openType;
}
static void resetSourceApplication() {
sourceApplication = null;
isOpenedByApplink = false;
}
/**
* Each app/device pair gets an GUID that is sent back with App Events and persisted with this
* app/device pair.
* @param context The application context.
* @return The GUID for this app/device pair.
*/
public static String getAnonymousAppDeviceGUID(Context context) {
if (anonymousAppDeviceGUID == null) {
synchronized (staticLock) {
if (anonymousAppDeviceGUID == null) {
SharedPreferences preferences = context.getSharedPreferences(
APP_EVENT_PREFERENCES,
Context.MODE_PRIVATE);
anonymousAppDeviceGUID = preferences.getString("anonymousAppDeviceGUID", null);
if (anonymousAppDeviceGUID == null) {
// Arbitrarily prepend XZ to distinguish from device supplied identifiers.
anonymousAppDeviceGUID = "XZ" + UUID.randomUUID().toString();
context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString("anonymousAppDeviceGUID", anonymousAppDeviceGUID)
.apply();
}
}
}
}
return anonymousAppDeviceGUID;
}
//
// 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 anonymousAppDeviceGUID;
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 anonymousGUID) {
this.attributionIdentifiers = identifiers;
this.packageName = packageName;
this.anonymousAppDeviceGUID = anonymousGUID;
}
// 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(GraphRequest request, boolean includeImplicitEvents,
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, 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(GraphRequest request, int numSkipped, JSONArray events,
boolean limitEventUsage) {
JSONObject publishParams = new JSONObject();
try {
publishParams.put("event", "CUSTOM_APP_EVENTS");
if (numSkippedEventsDueToFullBuffer > 0) {
publishParams.put("num_skipped_events", numSkipped);
}
Utility.setAppEventAttributionParameters(publishParams, attributionIdentifiers,
anonymousAppDeviceGUID, 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.put("application_package_name", packageName);
} catch (JSONException e) {
// Swallow
}
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
) {
try {
validateIdentifier(eventName);
this.name = eventName;
isImplicit = isImplicitlyLogged;
jsonObject = new JSONObject();
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");
}
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;
} catch (FacebookException e) {
// If any of the above failed, just consider this an illegal event.
Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
"Invalid app event name or parameter:", e.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) throws FacebookException {
// 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(
Locale.ROOT,
"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,
String sourceApplicationInfo
) {
synchronized (staticLock) {
FacebookTimeSpentData timeSpentData = getTimeSpentData(context, accessTokenAppId);
timeSpentData.onResume(logger, eventTime, sourceApplicationInfo);
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);
}
}
}