/**
* 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.internal;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import com.facebook.FacebookSdk;
import com.facebook.appevents.AppEventsLogger;
import com.facebook.internal.FetchedAppSettings;
import com.facebook.internal.FetchedAppSettingsManager;
import com.facebook.internal.Utility;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class ActivityLifecycleTracker {
private static final String TAG = ActivityLifecycleTracker.class.getCanonicalName();
private static final String INCORRECT_IMPL_WARNING = "Unexpected activity pause without a " +
"matching activity resume. Logging data may be incorrect. Make sure you call " +
"activateApp from your Application's onCreate method";
private static final long INTERRUPTION_THRESHOLD_MILLISECONDS = 1000;
private static final ScheduledExecutorService singleThreadExecutor =
Executors.newSingleThreadScheduledExecutor();
private static volatile ScheduledFuture currentFuture;
private static AtomicInteger foregroundActivityCount = new AtomicInteger(0);
// This member should only be changed or updated when executing on the singleThreadExecutor.
private static volatile SessionInfo currentSession;
private static AtomicBoolean tracking = new AtomicBoolean(false);
private static String appId;
private static long currentActivityAppearTime;
public static void startTracking(Application application, final String appId) {
if (!tracking.compareAndSet(false, true)) {
return;
}
ActivityLifecycleTracker.appId = appId;
application.registerActivityLifecycleCallbacks(
new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(
final Activity activity,
Bundle savedInstanceState) {
AppEventUtility.assertIsMainThread();
ActivityLifecycleTracker.onActivityCreated(activity);
}
@Override
public void onActivityStarted(Activity activity) {}
@Override
public void onActivityResumed(final Activity activity) {
AppEventUtility.assertIsMainThread();
ActivityLifecycleTracker.onActivityResumed(activity);
}
@Override
public void onActivityPaused(final Activity activity) {
AppEventUtility.assertIsMainThread();
ActivityLifecycleTracker.onActivityPaused(activity);
}
@Override
public void onActivityStopped(Activity activity) {
AppEventsLogger.onContextStop();
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
public static boolean isTracking() {
return tracking.get();
}
public static UUID getCurrentSessionGuid() {
return currentSession != null ? currentSession.getSessionId() : null;
}
// Public in order to allow unity sdk to correctly log app events
public static void onActivityCreated(Activity activity) {
final long currentTime = System.currentTimeMillis();
final Context applicationContext = activity.getApplicationContext();
final String activityName = Utility.getActivityName(activity);
final SourceApplicationInfo sourceApplicationInfo =
SourceApplicationInfo.Factory.create(activity);
Runnable handleActivityCreate = new Runnable() {
@Override
public void run() {
if (currentSession == null) {
SessionInfo lastSession =
SessionInfo.getStoredSessionInfo();
if (lastSession != null) {
SessionLogger.logDeactivateApp(
applicationContext,
activityName,
lastSession,
appId);
}
currentSession = new SessionInfo(currentTime, null);
currentSession.setSourceApplicationInfo(sourceApplicationInfo);
SessionLogger.logActivateApp(
applicationContext,
activityName,
sourceApplicationInfo,
appId);
}
}
};
singleThreadExecutor.execute(handleActivityCreate);
}
// Public in order to allow unity sdk to correctly log app events
public static void onActivityResumed(Activity activity) {
foregroundActivityCount.incrementAndGet();
cancelCurrentTask();
final long currentTime = System.currentTimeMillis();
ActivityLifecycleTracker.currentActivityAppearTime = currentTime;
final Context applicationContext = activity.getApplicationContext();
final String activityName = Utility.getActivityName(activity);
Runnable handleActivityResume = new Runnable() {
@Override
public void run() {
if (currentSession == null) {
currentSession = new SessionInfo(currentTime, null);
SessionLogger.logActivateApp(
applicationContext,
activityName,
null,
appId);
} else if (currentSession.getSessionLastEventTime() != null) {
long suspendTime =
currentTime - currentSession.getSessionLastEventTime();
if (suspendTime > getSessionTimeoutInSeconds() * 1000) {
// We were suspended for a significant amount of time.
// Count this as a new session and log the old session
SessionLogger.logDeactivateApp(
applicationContext,
activityName,
currentSession,
appId);
SessionLogger.logActivateApp(
applicationContext,
activityName,
null,
appId);
currentSession = new SessionInfo(currentTime, null);
} else if (suspendTime > INTERRUPTION_THRESHOLD_MILLISECONDS) {
currentSession.incrementInterruptionCount();
}
}
currentSession.setSessionLastEventTime(currentTime);
currentSession.writeSessionToDisk();
}
};
singleThreadExecutor.execute(handleActivityResume);
}
private static void onActivityPaused(Activity activity) {
int count = foregroundActivityCount.decrementAndGet();
if (count < 0) {
// Our ref count can be off if a developer doesn't call activate
// app from the Application's onCreate method.
foregroundActivityCount.set(0);
Log.w(TAG, INCORRECT_IMPL_WARNING);
}
cancelCurrentTask();
final long currentTime = System.currentTimeMillis();
// Pull out this information now to avoid holding a reference to the activity
final Context applicationContext = activity.getApplicationContext();
final String activityName = Utility.getActivityName(activity);
Runnable handleActivityPaused = new Runnable() {
@Override
public void run() {
if (currentSession == null) {
// This can happen if a developer doesn't call activate
// app from the Application's onCreate method
currentSession = new SessionInfo(currentTime, null);
}
currentSession.setSessionLastEventTime(currentTime);
if (foregroundActivityCount.get() <= 0) {
// Schedule check to see if we still have 0 foreground
// activities in our set time. This indicates that the app has
// been backgrounded
Runnable task = new Runnable() {
@Override
public void run() {
if (foregroundActivityCount.get() <= 0) {
SessionLogger.logDeactivateApp(
applicationContext,
activityName,
currentSession,
appId);
SessionInfo.clearSavedSessionFromDisk();
currentSession = null;
}
currentFuture = null;
}
};
currentFuture = singleThreadExecutor.schedule(
task,
getSessionTimeoutInSeconds(),
TimeUnit.SECONDS);
}
long appearTime = ActivityLifecycleTracker.currentActivityAppearTime;
long timeSpentOnActivityInSeconds = appearTime > 0
? (currentTime - appearTime) / 1000
: 0;
AutomaticAnalyticsLogger.logActivityTimeSpentEvent(
activityName,
timeSpentOnActivityInSeconds
);
currentSession.writeSessionToDisk();
}
};
singleThreadExecutor.execute(handleActivityPaused);
}
private static int getSessionTimeoutInSeconds() {
FetchedAppSettings settings =
FetchedAppSettingsManager.getAppSettingsWithoutQuery(FacebookSdk.getApplicationId());
if (settings == null) {
return Constants.getDefaultAppEventsSessionTimeoutInSeconds();
}
return settings.getSessionTimeoutInSeconds();
}
private static void cancelCurrentTask() {
if (currentFuture != null) {
currentFuture.cancel(false);
}
currentFuture = null;
}
}