package com.facebook; import android.os.Bundle; import android.text.format.DateUtils; import com.facebook.internal.Logger; import java.io.Serializable; class FacebookTimeSpentData implements Serializable { // Constants private static final long serialVersionUID = 1L; private static final String TAG = AppEventsLogger.class.getCanonicalName(); private static final long FIRST_TIME_LOAD_RESUME_TIME = -1; private static final long INTERRUPTION_THRESHOLD_MILLISECONDS = 1000; private static final long NUM_MILLISECONDS_IDLE_TO_BE_NEW_SESSION = 60 * DateUtils.SECOND_IN_MILLIS; private static final long APP_ACTIVATE_SUPPRESSION_PERIOD_IN_MILLISECONDS = 5 * DateUtils.MINUTE_IN_MILLIS; // Should be kept in sync with the iOS sdk private static final long[] INACTIVE_SECONDS_QUANTA = new long[] { 5 * DateUtils.MINUTE_IN_MILLIS, 15 * DateUtils.MINUTE_IN_MILLIS, 30 * DateUtils.MINUTE_IN_MILLIS, 1 * DateUtils.HOUR_IN_MILLIS, 6 * DateUtils.HOUR_IN_MILLIS, 12 * DateUtils.HOUR_IN_MILLIS, 1 * DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, 3 * DateUtils.DAY_IN_MILLIS, 7 * DateUtils.DAY_IN_MILLIS, 14 * DateUtils.DAY_IN_MILLIS, 21 * DateUtils.DAY_IN_MILLIS, 28 * DateUtils.DAY_IN_MILLIS, 60 * DateUtils.DAY_IN_MILLIS, 90 * DateUtils.DAY_IN_MILLIS, 120 * DateUtils.DAY_IN_MILLIS, 150 * DateUtils.DAY_IN_MILLIS, 180 * DateUtils.DAY_IN_MILLIS, 365 * DateUtils.DAY_IN_MILLIS, }; private boolean isWarmLaunch; private boolean isAppActive; private long lastActivateEventLoggedTime; // Member data that's persisted to disk private long lastResumeTime; private long lastSuspendTime; private long millisecondsSpentInSession; private int interruptionCount; private String firstOpenSourceApplication; /** * Serialization proxy for the FacebookTimeSpentData class. This is version 1 of * serialization. Future serializations may differ in format. This * class should not be modified. If serializations formats change, * create a new class SerializationProxyVx. */ private static class SerializationProxyV1 implements Serializable { private static final long serialVersionUID = 6L; private final long lastResumeTime; private final long lastSuspendTime; private final long millisecondsSpentInSession; private final int interruptionCount; SerializationProxyV1( long lastResumeTime, long lastSuspendTime, long millisecondsSpentInSession, int interruptionCount ) { this.lastResumeTime = lastResumeTime; this.lastSuspendTime = lastSuspendTime; this.millisecondsSpentInSession = millisecondsSpentInSession; this.interruptionCount = interruptionCount; } private Object readResolve() { return new FacebookTimeSpentData( lastResumeTime, lastSuspendTime, millisecondsSpentInSession, interruptionCount); } } /** * Constructor to be used for V1 serialization only, DO NOT CHANGE. */ private FacebookTimeSpentData( long lastResumeTime, long lastSuspendTime, long millisecondsSpentInSession, int interruptionCount ) { resetSession(); this.lastResumeTime = lastResumeTime; this.lastSuspendTime = lastSuspendTime; this.millisecondsSpentInSession = millisecondsSpentInSession; this.interruptionCount = interruptionCount; } /** * Serialization proxy for the FacebookTimeSpentData class. This is version 2 of * serialization. Future serializations may differ in format. This * class should not be modified. If serializations formats change, * create a new class SerializationProxyVx. */ private static class SerializationProxyV2 implements Serializable { private static final long serialVersionUID = 6L; private final long lastResumeTime; private final long lastSuspendTime; private final long millisecondsSpentInSession; private final int interruptionCount; private final String firstOpenSourceApplication; SerializationProxyV2( long lastResumeTime, long lastSuspendTime, long millisecondsSpentInSession, int interruptionCount, String firstOpenSourceApplication ) { this.lastResumeTime = lastResumeTime; this.lastSuspendTime = lastSuspendTime; this.millisecondsSpentInSession = millisecondsSpentInSession; this.interruptionCount = interruptionCount; this.firstOpenSourceApplication = firstOpenSourceApplication; } private Object readResolve() { return new FacebookTimeSpentData( lastResumeTime, lastSuspendTime, millisecondsSpentInSession, interruptionCount, firstOpenSourceApplication); } } FacebookTimeSpentData() { resetSession(); } /** * Constructor to be used for V2 serialization only, DO NOT CHANGE. */ private FacebookTimeSpentData( long lastResumeTime, long lastSuspendTime, long millisecondsSpentInSession, int interruptionCount, String firstOpenSourceApplication ) { resetSession(); this.lastResumeTime = lastResumeTime; this.lastSuspendTime = lastSuspendTime; this.millisecondsSpentInSession = millisecondsSpentInSession; this.interruptionCount = interruptionCount; this.firstOpenSourceApplication = firstOpenSourceApplication; } private Object writeReplace() { return new SerializationProxyV2( lastResumeTime, lastSuspendTime, millisecondsSpentInSession, interruptionCount, firstOpenSourceApplication ); } void onSuspend(AppEventsLogger logger, long eventTime) { if (!isAppActive) { Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Suspend for inactive app"); return; } long now = eventTime; long delta = (now - lastResumeTime); if (delta < 0) { Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Clock skew detected"); delta = 0; } millisecondsSpentInSession += delta; lastSuspendTime = now; isAppActive = false; } void onResume(AppEventsLogger logger, long eventTime, String sourceApplicationInfo) { long now = eventTime; // Retain old behavior for activated app event - log the event if the event hasn't // been logged in the previous suppression interval or this is a cold launch. // If this is a cold launch, always log the event. Otherwise, use the last // event log time to determine if the app activate should be suppressed or not. if (isColdLaunch() || ((now - lastActivateEventLoggedTime) > APP_ACTIVATE_SUPPRESSION_PERIOD_IN_MILLISECONDS)) { Bundle eventParams = new Bundle(); eventParams.putString( AppEventsConstants.EVENT_PARAM_SOURCE_APPLICATION, sourceApplicationInfo); logger.logEvent(AppEventsConstants.EVENT_NAME_ACTIVATED_APP, eventParams); lastActivateEventLoggedTime = now; } // If this is an application that's not calling onSuspend yet, log and return. We can't // track time spent for this application as there are no calls to onSuspend. if (isAppActive) { Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Resume for active app"); return; } long interruptionDurationMillis = wasSuspendedEver() ? now - lastSuspendTime : 0; if (interruptionDurationMillis < 0) { Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Clock skew detected"); interruptionDurationMillis = 0; } // If interruption duration is > new session threshold, then log old session // event and start a new session. if (interruptionDurationMillis > NUM_MILLISECONDS_IDLE_TO_BE_NEW_SESSION) { logAppDeactivatedEvent(logger, interruptionDurationMillis); } else { // We're not logging this resume event - check to see if this should count // as an interruption if (interruptionDurationMillis > INTERRUPTION_THRESHOLD_MILLISECONDS) { interruptionCount++; } } // Set source application only for the first resume of the timespent session. if (interruptionCount == 0) { firstOpenSourceApplication = sourceApplicationInfo; } lastResumeTime = now; isAppActive = true; } private void logAppDeactivatedEvent(AppEventsLogger logger, long interruptionDurationMillis) { // Log the old session information and clear the data Bundle eventParams = new Bundle(); eventParams.putInt( AppEventsConstants.EVENT_NAME_SESSION_INTERRUPTIONS, interruptionCount); eventParams.putString( AppEventsConstants.EVENT_NAME_TIME_BETWEEN_SESSIONS, String.format("session_quanta_%d", getQuantaIndex(interruptionDurationMillis))); eventParams.putString( AppEventsConstants.EVENT_PARAM_SOURCE_APPLICATION, firstOpenSourceApplication); logger.logEvent( AppEventsConstants.EVENT_NAME_DEACTIVATED_APP, (millisecondsSpentInSession/DateUtils.SECOND_IN_MILLIS), eventParams); resetSession(); } private static int getQuantaIndex(long timeBetweenSessions) { int quantaIndex = 0; while ( quantaIndex < INACTIVE_SECONDS_QUANTA.length && INACTIVE_SECONDS_QUANTA[quantaIndex] < timeBetweenSessions ) { ++quantaIndex; } return quantaIndex; } private void resetSession() { isAppActive = false; lastResumeTime = FIRST_TIME_LOAD_RESUME_TIME; lastSuspendTime = FIRST_TIME_LOAD_RESUME_TIME; interruptionCount = 0; millisecondsSpentInSession = 0; } private boolean wasSuspendedEver() { return lastSuspendTime != FIRST_TIME_LOAD_RESUME_TIME; } private boolean isColdLaunch() { // On the very first call in the process lifecycle, this will always // return true. After that, it will always return false. boolean result = !isWarmLaunch; isWarmLaunch = true; return result; } }