package com.mfh.comna.utils;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.format.DateUtils;
import com.mfh.comna.api.utils.MLog;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;
/**
* This class is responsible for tracking all currently open activities.
* By doing so this class can detect when the application is in the foreground
* and when it is running in the background.
*/
public class AppRunningStateManager {
private static final String TAG = AppRunningStateManager.class.getSimpleName();
private static final int MESSAGE_NOTIFY_LISTENERS = 1;
public static final long APP_CLOSED_VALIDATION_TIME_IN_MS = 5 * DateUtils.SECOND_IN_MILLIS; // 5 Seconds
private Reference<Activity> mForegroundActivity;
public enum AppRunningState {
FOREGROUND,
BACKGROUND
}
private AppRunningState mAppRunningState = AppRunningState.BACKGROUND;
public interface OnAppRunningStateChangeListener {
/** Called when the running state of the app changes */
public void onAppRunningStateChange(AppRunningState newState);
}
private Set<OnAppRunningStateChangeListener> mListeners = new HashSet<OnAppRunningStateChangeListener>();
private NotifyListenersHandler mHandler;
// Make this class a thread safe singleton
private static class SingletonHolder {
public static final AppRunningStateManager INSTANCE = new AppRunningStateManager();
}
public static AppRunningStateManager getInstance() {
return SingletonHolder.INSTANCE;
}
private AppRunningStateManager() {
// Create the handler on the main thread
mHandler = new NotifyListenersHandler(Looper.getMainLooper());
}
/** An activity should call this when it becomes visible */
public void onActivityVisible(Activity activity) {
if (mForegroundActivity != null) {
mForegroundActivity.clear();
}
mForegroundActivity = new WeakReference<Activity>(activity);
determineAppRunningState();
}
/** An activity should call this when it is no longer visible */
public void onActivityInvisible(Activity activity) {
/*
* The foreground activity may have been replaced with a new foreground activity in our app.
* So only clear the foregroundActivity if the new activity matches the foreground activity.
*/
if (mForegroundActivity != null) {
Activity ref = mForegroundActivity.get();
if (activity == ref) {
// This is the activity that is going away, clear the reference
mForegroundActivity.clear();
mForegroundActivity = null;
}
}
determineAppRunningState();
}
/**
* Call to determine the current state, update the tracking global, and notify subscribers if the state has changed.
*/
private void determineAppRunningState() {
/* Get the current state */
AppRunningState oldState = mAppRunningState;
/* Determine what the new state should be */
final boolean isInForeground = mForegroundActivity != null && mForegroundActivity.get() != null;
mAppRunningState = isInForeground ? AppRunningState.FOREGROUND : AppRunningState.BACKGROUND;
/* If the new state is different then the old state the notify subscribers of the state change */
if (mAppRunningState != oldState) {
validateThenNotifyListeners();
}
}
/**
* This method will notify subscribes that the foreground state has changed when and if appropriate.
* <br><br>
* We do not want to just notify listeners right away when the app enters of leaves the foreground. When changing orientations or opening and
* closing the app quickly we briefly pass through a NOT_IN_FOREGROUND state that must be ignored. To accomplish this a delayed message will be
* Sent when we detect a change. We will not notify that a foreground change happened until the delay time has been reached. If a second
* foreground change is detected during the delay period then the notification will be canceled.
*/
private void validateThenNotifyListeners() {
// If the app has any pending notifications then throw out the event as the state change has failed validation
if (mHandler.hasMessages(MESSAGE_NOTIFY_LISTENERS)) {
MLog.v("Validation Failed: Throwing out app foreground state change notification");
mHandler.removeMessages(MESSAGE_NOTIFY_LISTENERS);
} else {
if (mAppRunningState == AppRunningState.FOREGROUND) {
// If the app entered the foreground then notify listeners right away; there is no validation time for this
mHandler.sendEmptyMessage(MESSAGE_NOTIFY_LISTENERS);
} else {
// We need to validate that the app entered the background. A delay is used to allow for time when the application went into the
// background but we do not want to consider the app being backgrounded such as for in app purchasing flow and full screen ads.
mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY_LISTENERS, APP_CLOSED_VALIDATION_TIME_IN_MS);
}
}
}
/**
* Add a listener to be notified of app foreground state change events.
*
* @param listener
*/
public void addListener(OnAppRunningStateChangeListener listener) {
mListeners.add(listener);
}
/**
* Remove a listener from being notified of app foreground state change events.
*
* @param listener
*/
public void removeListener(OnAppRunningStateChangeListener listener) {
mListeners.remove(listener);
}
/** Notify all listeners the app running state has changed */
private void notifyListeners(AppRunningState newState) {
android.util.Log.i(TAG, "Notifying subscribers that app just entered state: " + newState);
for (OnAppRunningStateChangeListener listener : mListeners) {
listener.onAppRunningStateChange(newState);
}
}
private class NotifyListenersHandler extends Handler {
private NotifyListenersHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message inputMessage) {
switch (inputMessage.what) {
// The decoding is done
case MESSAGE_NOTIFY_LISTENERS:
/* Notify subscribers of the state change */
MLog.v(TAG, "App just changed running state to: " + mAppRunningState);
notifyListeners(mAppRunningState);
break;
default:
super.handleMessage(inputMessage);
}
}
}
}