org.restcomm.app.qoslib.Services.Events.EventManager.java Source code

Java tutorial

Introduction

Here is the source code for org.restcomm.app.qoslib.Services.Events.EventManager.java

Source

/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2016, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 * For questions related to commercial use licensing, please contact sales@telestax.com.
 *
 */

package org.restcomm.app.qoslib.Services.Events;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import android.content.ContentValues;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.preference.PreferenceManager;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;

import org.restcomm.app.qoslib.MainService;
import org.restcomm.app.utillib.ContentProvider.ContentValuesGenerator;
import org.restcomm.app.utillib.ContentProvider.TablesEnum;
import org.restcomm.app.utillib.DataObjects.EventCouple;
import org.restcomm.app.utillib.DataObjects.EventObj;
import org.restcomm.app.utillib.DataObjects.EventTypeGenre;
import org.restcomm.app.utillib.DataObjects.PhoneState;
import org.restcomm.app.utillib.Reporters.ReportManager;
import org.restcomm.app.utillib.Utils.Global;
import org.restcomm.app.utillib.Utils.GpsListener;
import org.restcomm.app.utillib.Utils.LoggerUtil;
import org.restcomm.app.mmcextension.EventTriggers.SpeedTestTrigger;
import org.restcomm.app.mmcextension.EventTriggers.VideoTestTrigger;
import org.restcomm.app.utillib.Reporters.LocalStorageReporter.LocalStorageReporter;
import org.restcomm.app.utillib.DataObjects.CellLocationEx;
import org.restcomm.app.utillib.DataObjects.SignalEx;
import org.restcomm.app.qoslib.Utils.QosAPI;
import org.restcomm.app.utillib.Utils.DeviceInfoOld;
import org.restcomm.app.utillib.DataObjects.EventType;
import org.restcomm.app.utillib.Utils.PreferenceKeys;
import org.restcomm.app.utillib.DataObjects.EventDataEnvelope;
import org.restcomm.app.utillib.Utils.UsageLimits;

import org.json.JSONException;
import org.json.JSONObject;

import org.restcomm.app.qoslib.R;

/**
 * This is the class responsible for the event related logic that has to be executed
 * on the app. Some of its main features are :-
 * <ul>
 *    <li>Storing the events (in both the SQLite tables and the internal caches).</li>
 *    <li>Responding to queries about the various events quickly.</li>
 *    <li>Associating other pieces of data (like the locations, the signal strengths and the
 * base stations) with the corresponding events.</li>
 * </ul>
 * 
 * Note: In this version of the event manager, we are not going to calculate the durations
 * of the events. Instead we are just going to declare the durations zero. This is because the
 * new server would not require calculation of these durations and we have to move to the new
 * server eventually anyways.
 * @author Abhin
 *
 */
public class EventManager {
    public static final String TAG = EventManager.class.getSimpleName();
    /**
     * When an event occurs (whether it belongs to an event couple or not), it is able to accept
     * data (like signal strength information) for a definite period of time after its start.
     * This period is called the lime light period of the event.
     */
    public static final int EVENT_STAGING_PERIOD = 30000; //in milliseconds

    /**
     * This is a list of ongoing events
     */
    private List<EventCouple> ongoingEventCouples = new ArrayList<EventCouple>();
    private List<EventObj> ongoingEvents = new ArrayList<EventObj>();
    /**
     * This variable stores the currently staged event ("staged" would be referred to as a phase of an event). During
     * this phase, the event is able to associate additional data with itself.
     * 
     * Note:- There can only be one event at a time that can be considered "staged". That event is 
     * whichever event this variable stores.
     * @see EventManager#EVENT_STAGING_PERIOD
     */
    private EventObj stagedEvent = null;

    /**
     * This variable stores the complimentary event of the event that is currently stored. In case the staged event 
     * is the "starting end" of the eventCouple or the staged event is a singleton, this variable would be null.
     */
    private EventObj stagedComplimentaryEvent = null;

    /**
     * This is the timer than "kicks events off of the stage" when their time is up.
     */
    private Timer stageTimer = new Timer();

    /**
     * This is the timer that uploads the event after the designated delay.
     */
    private Timer uploadTimer = new Timer();

    /**
     * This is an instance of the StageTimerTask class that is being stored in this
     * variable to make the process of resetting the timer easier.
     */
    private StageTimerTask stageTimerTask;

    /**
     * The eventCache holds all the eventTypes that are currently active
     */
    private EnumSet<EventType> eventCache = EnumSet.noneOf(EventType.class);
    // Queue events if unable to send to server
    private static ConcurrentLinkedQueue<EventDataEnvelope> eventQueue = null;

    private MainService context;
    protected long mLastServiceStateChangeTimeStamp = 0, mLast3GStateChangeTimeStamp = 0,
            mLastScreenOffDuration = 0;
    private SpeedTestTrigger mSpeedTestTrigger;
    private VideoTestTrigger mVideoTestTrigger;
    private EventObj trackingEvent = null;

    public EventManager(MainService context) {
        this.context = context;
        mLastServiceStateChangeTimeStamp = System.currentTimeMillis();
        mLast3GStateChangeTimeStamp = System.currentTimeMillis();
        mSpeedTestTrigger = new SpeedTestTrigger(context.getCallbacks(), context.handler);
        mVideoTestTrigger = new VideoTestTrigger(context.getCallbacks(), context.handler);
    }

    /**
     * Cancels timers used by this class, as they do not get cancelled when the object is garbage collected.
     */
    public void stop() {
        stageTimer.cancel();
        uploadTimer.cancel();
        mSpeedTestTrigger.stop();
        mVideoTestTrigger.stop();
    }

    /**
     * This method starts an event couple and updates the cache. The steps it takes are :-
     * <ol>
     *    <li>Creates an entry in the Event table with the appropriate eventType and 0 duration.</li>
     *    <li>Creates an entry in the EventCouple Table with the stopEvent field null.</li>
     *    <li>Using the uris it gets from the above 2 inserts, it creates the event couple and adds it to {@link #ongoingEventCouples}</li>
     *    <li>Now it "flags" the event in the {@link #eventCache} for quick access.</li>
     * </ol>
     * <b>Note: This method assumes that the event couple is not already running.</b>
     * <b>Note: This method does not stage the event. That part has to be done separately.</b>
     * @param startEventType The event type of the starting event.
     * @param stopEventType The event type of the ending event.
     * @return eventCouple The event couple that was just created
     */
    public EventCouple beginEventCouple(EventType startEventType, EventType stopEventType) {
        return beginEventCouple(startEventType, stopEventType, System.currentTimeMillis());
    }

    public EventCouple beginEventCouple(EventType startEventType, EventType stopEventType, long time) {
        //create the event couple and add it to the ongoingEvents list
        EventCouple eventCouple = new EventCouple(startEventType, stopEventType, null, null);//eventCoupleUri, startEventUri);
        ongoingEventCouples.add(eventCouple);

        //flag the event in the event cache
        eventCache.add(startEventType);
        signalSnapshot(eventCouple.getStartEvent());
        return eventCouple;
    }

    /**
     * This method stops an event pair and updates the cache.
     * The startEventType and stopEventType define the type of event pair, such as Lost/Regained Service
     * this method ensures that the start of the event couple is already running
     * @param startEventType
     * @param stopEventType
     * @return
     */
    public EventCouple endEventCouple(EventType startEventType, EventType stopEventType) {

        // move the event out of the ongoing event couples list
        EventCouple completedEventCouple = getEventCouple(startEventType, stopEventType);
        if (completedEventCouple == null) {
            return null; //if the event doesn't exist in the list, then forget it
        }
        completedEventCouple.triggerStopEvent(0);

        // remove the event from the internal event cache and the ongoing event list
        eventCache.remove(startEventType); //since the startEventTypes of the events in the list are going to be unique
        //all the time, the flag can safely be removed from the eventCache.
        ongoingEventCouples.remove(completedEventCouple);
        eventCache.add(stopEventType); // the 2nd event of couple is still ongoing using the gps
        signalSnapshot(completedEventCouple.getStopEvent());

        return completedEventCouple;
    }

    public void stopTracking() {
        unstageAndUploadEvent(trackingEvent, null);
        if (trackingEvent != null && trackingEvent.gpsListener != null)
            context.getGpsManager().unregisterListener(trackingEvent.gpsListener);
        context.getTrackingManager().cancelScheduledEvents();
    }

    /**
     * This method :-
     * <ol>
     *    <li>Notifies the {@link EventManager} to register the event.</li>
     *    <li>Dispatches this information to the UI in case it wants to draw a marker for it on the chart.</li>
     *    <li>Starts the GPS.</li>
     * </ol>
     * @param eventType
     */
    public EventObj triggerSingletonEvent(EventType eventType) {
        if ((eventType == EventType.EVT_CALLFAIL || eventType == EventType.EVT_DROP)
                && EventObj.isDisabledEvent(context, EventObj.DISABLE_DROPCALL))
            return null;
        final EventObj singletonEvent = registerSingletonEvent(eventType);

        if (eventType == EventType.MAN_TRACKING)
            trackingEvent = singletonEvent;
        runEvent(singletonEvent, null, false);
        return singletonEvent;
    }

    /**
     * This method starts a standalone event, and event that is not a part of a pair
     * @param eventType
     * @return
     */
    public EventObj registerSingletonEvent(EventType eventType) {
        //now move the event into the "staged" phase
        EventObj event = null;
        event = new EventObj(eventType, null); //endEventUri);

        //and now flag the event in the event cache for quick access
        eventCache.add(eventType); //if the event is already flagged in the eventCache, then nothing will happen
        signalSnapshot(event);

        return event;
    }

    /**
     * Start a paired event. Specifies the start and stop events to define the paired event (EventCouple)
     */

    public EventObj startPhoneEvent(EventType startEventType, EventType stopEventType) {
        if (startEventType == EventType.COV_4G_NO)
            context.getIntentDispatcher().updateLTEIdentity(null);

        return startPhoneEvent(startEventType, stopEventType, System.currentTimeMillis());
    }

    public EventObj startPhoneEvent(EventType startEventType, EventType stopEventType, long time) {
        // cancel any phone related event when in dormant mode
        if (getUsageLimits().getDormantMode() >= 1)
            return null;
        if (getUsageLimits().getUsageProfile() <= UsageLimits.MINIMAL) {
            // Lets not return these data outages during minimal mode.
            if (startEventType == EventType.COV_3G_NO || startEventType == EventType.COV_4G_NO
                    || startEventType == EventType.COV_DATA_NO)
                return null;
        }
        if (!context.getPhoneStateListener().validSignal && (startEventType == EventType.COV_4G_NO
                || startEventType == EventType.COV_3G_NO || startEventType == EventType.COV_DATA_NO
                || startEventType == EventType.COV_4G_NO || startEventType == EventType.COV_VOD_NO))
            // phone must see at least one valid signal before allowing these events
            return null;

        // These events can also be disabled by the event response
        if ((startEventType == EventType.COV_3G_NO && EventObj.isDisabledEvent(context, EventObj.DISABLE_LOST3G))
                || startEventType == EventType.COV_DATA_NO
                        && EventObj.isDisabledEvent(context, EventObj.DISABLE_LOST2G)
                || startEventType == EventType.COV_4G_NO
                        && EventObj.isDisabledEvent(context, EventObj.DISABLE_LOST4G)
                || startEventType == EventType.COV_VOD_NO
                        && EventObj.isDisabledEvent(context, EventObj.DISABLE_LOSTSVC)
                || startEventType == EventType.EVT_CONNECT
                        && EventObj.isDisabledEvent(context, EventObj.DISABLE_ALLCALL)
                || startEventType == EventType.EVT_CONNECT
                        && EventObj.isDisabledEvent(context, EventObj.DISABLE_NONDROPCALL))
            return null;

        if (this.isEventRunning(startEventType)) {
            if (startEventType == EventType.SIP_CONNECT) {
                EventCouple targetEventCouple = getEventCouple(startEventType, stopEventType);
                if (targetEventCouple != null)
                    this.unstageEvent(targetEventCouple.getStartEvent());
            } else
                //if the event is somehow already running, then it cannot be made to run again
                //this method will now simply return
                return null; // This could be very bad if it somehow misses a Disconnect and its never allowed to trigger a connect/disconnect event again
        }
        EventCouple eventCouple = this.beginEventCouple(startEventType, stopEventType, time);
        runEvent(eventCouple.getStartEvent(), eventCouple.getStopEvent(), false);
        return eventCouple.getStartEvent();
    }

    /**
     * This method :-
     * <ol>
     *    <li>Notifies the {@link EventManager} to stop the event.</li>
     *    <li>Dispatches this information to the UI in case it wants to draw a marker for it on the chart.</li>
     *    <li>Starts the GPS.</li>
     * </ol>
     */
    public EventObj stopPhoneEvent(EventType startEventType, EventType stopEventType) {
        // cancel any phone related event when in dormant mode
        if (getUsageLimits().getDormantMode() >= 1)
            return null;

        if (!this.isEventRunning(startEventType)) {
            //if the start event of this couple is somehow not running, then the stop event can't run
            //this method will now simply return
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "stopPhoneEvent",
                    "missing start of pair " + startEventType.name());
            return null;
        }
        EventCouple eventCouple = this.endEventCouple(startEventType, stopEventType);
        long duration = 0;
        if (eventCouple != null && eventCouple.getStopEvent() != null && eventCouple.getStartEvent() != null) {
            duration = eventCouple.getStopEvent().getEventTimestamp()
                    - eventCouple.getStartEvent().getEventTimestamp();
            if (eventCouple.getStartEventType() == EventType.EVT_CONNECT
                    || eventCouple.getStartEventType() == EventType.SIP_CONNECT) {
                eventCouple.getStopEvent().setDuration(duration);
                eventCouple.getStartEvent().setDuration(duration);
            }
        } else if (eventCouple == null) {
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "stopPhoneEvent", "eventCouple=null");
            return null;
        }
        boolean bUnstage = false;
        int cutoff = 30; // 30 second cutoff on most paired events
        if (stopEventType == EventType.COV_VOD_YES)
            cutoff = 15; // shorter cutoff on voice outage events, which means we want to try for the full gps fix on a 15 second outage
        if (getUsageLimits().getUsageProfile() == 0)
            cutoff = cutoff * 4 / 3; // minimum profile will require the outage or event pair to last longer to allow a full gps timeout
        else if (getUsageLimits().getUsageProfile() >= 2)
            cutoff = 0; // maximum profile will allow a full gps timeout on every paired event no matter how short

        if (duration < cutoff * 1000 && !context.getTravelDetector().isTravelling()
                && (stopEventType == EventType.COV_3G_YES || stopEventType == EventType.COV_4G_YES
                        || stopEventType == EventType.COV_DATA_YES || stopEventType == EventType.COV_VOD_YES
                        || stopEventType == EventType.EVT_DISCONNECT || stopEventType == EventType.SIP_DISCONNECT))
            bUnstage = true;

        // When a call ends, unstage the connect call
        if ((stopEventType == EventType.EVT_DROP || stopEventType == EventType.EVT_DISCONNECT
                || stopEventType == EventType.EVT_CALLFAIL || stopEventType == EventType.EVT_UNANSWERED
                || stopEventType == EventType.SIP_DROP || stopEventType == EventType.SIP_DISCONNECT
                || stopEventType == EventType.SIP_CALLFAIL || stopEventType == EventType.SIP_UNANSWERED)) {
            LoggerUtil.logToFile(LoggerUtil.Level.ERROR, TAG, "stopPhoneEvent", "bUnstage = true ");
        }

        runEvent(eventCouple.getStopEvent(), eventCouple.getStartEvent(), bUnstage);
        return eventCouple.getStopEvent();

    }

    public void runEvent(final EventObj event, final EventObj compEvent, final boolean bUnstage) {
        final EventType eventType = event.getEventType();

        final boolean bRunEvent;
        // decide whether to let event run the GPS and set mmc active
        if (event.getEventType().getPostEventStageTime() > 0 //  && !getUsageLimits ().exceededGps (event.getEventType(), true)
                && (!bUnstage || event.getEventType() == EventType.EVT_DROP
                        || event.getEventType() == EventType.EVT_DISCONNECT
                        || event.getEventType() == EventType.SIP_DROP
                        || event.getEventType() == EventType.SIP_DISCONNECT)) // Only invoke Gps if EventType needs GPS (not for normal disconnect)
        {
            bRunEvent = true;

            if (event.getEventType() != EventType.COV_UPDATE && event.getEventType() != EventType.TRAVEL_CHECK
                    && event.getEventType() != EventType.LATENCY_TEST
                    && event.getEventType() != EventType.WIFI_CONNECT
                    && event.getEventType() != EventType.WIFI_DISCONNECT
                    && event.getEventType() != EventType.LATENCY_TEST)
                context.keepAwake(true, false); // use a screen-on wake lock for most events
            else
                context.keepAwake(true, true); // but use a partial wake lock for update and travel events so it doesnt look like screen turns on mysteriously every 3 hours
        } else
            bRunEvent = false;
        context.handler.post(new Runnable() {
            // @Override
            public void run() {
                try {
                    if (context.isServiceRunning() == false) {
                        LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "runEvent", "service not running ");
                        return;
                    }
                    LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "runEvent",
                            "starting event type " + eventType.getEventName());

                    context.getIntentDispatcher().markEvent(eventType);
                    event.setAppName(context.getDataMonitorStats().getForegroundAppName());

                    if (bRunEvent) // Only invoke Gps if EventType needs GPS (not for normal disconnect)
                    {
                        String reason = null;
                        if (eventType == EventType.EVT_FILLIN) {
                            reason = null; // "mapping new coverage area";
                        } else if (eventType == EventType.COV_3G_NO || eventType == EventType.COV_3G_YES
                                || eventType == EventType.COV_4G_NO || eventType == EventType.COV_4G_YES
                                || eventType == EventType.COV_DATA_NO || eventType == EventType.COV_DATA_YES
                                || eventType == EventType.COV_VOD_NO || eventType == EventType.COV_VOD_YES
                                || eventType == EventType.EVT_DROP || eventType == EventType.EVT_CALLFAIL
                                || eventType == EventType.MAN_SPEEDTEST || eventType == EventType.SIP_DROP
                                || eventType == EventType.SIP_CALLFAIL || eventType == EventType.SMS_TEST
                                || eventType == EventType.APP_MONITORING || eventType == EventType.VIDEO_TEST
                                || eventType == EventType.YOUTUBE_TEST || eventType == EventType.AUDIO_TEST
                                || eventType == EventType.WEBPAGE_TEST || eventType == EventType.EVT_VQ_CALL)
                            reason = eventType.getEventString(context);
                        else if (eventType == EventType.MAN_TRACKING)
                            reason = EventType.MAN_TRACKING.getEventString(context);// getString(R.string.Gene"recording";
                        else if (eventType == EventType.COV_UPDATE || eventType == EventType.TRAVEL_CHECK
                                || eventType == EventType.LATENCY_TEST || eventType == EventType.WIFI_CONNECT
                                || eventType == EventType.WIFI_DISCONNECT)
                            reason = "background";
                        else
                            reason = null;

                        int allowPopup = PreferenceManager.getDefaultSharedPreferences(context)
                                .getInt(PreferenceKeys.Miscellaneous.ALLOW_DROP_POPUP, 2);
                        if (allowPopup == 1 && !context.getUseRadioLog())
                            allowPopup = 0;
                        if (allowPopup == 0
                                && (eventType == EventType.EVT_DROP || eventType == EventType.EVT_CALLFAIL
                                        || eventType == EventType.SIP_DROP || eventType == EventType.SIP_CALLFAIL))
                            reason = context.getString(R.string.GenericText_Call_logged);

                        //else if (eventType == EventType.COV_UPDATE)
                        //   reason = "";
                        //else
                        //   reason = "checking coverage";
                        context.startRadioLog(true, reason, eventType);
                        if (event.getEventType() == EventType.MAN_PLOTTING
                                || event.getEventType() == EventType.MAN_TRANSIT)
                            return;
                        if (event.gpsListener == null) {
                            event.gpsListener = new GpsListenerForEvent(event, compEvent);
                        }
                        context.getGpsManager().registerListener(event.gpsListener);
                        context.getNetLocationManager().registerListener(new LocationListenerForEvent(event)); // contingency coarse location listener that will try to write first coarse location to same location database

                    } else if (event.getEventType().getGenre() == EventTypeGenre.END_OF_COUPLE) {
                        //Location lastLocation = null;
                        //if (event.gpsListener != null)
                        //   lastLocation = event.gpsListener.getLastLocation ();
                        context.getGpsManager().unregisterListener(compEvent.gpsListener);
                        temporarilyStageEvent(event, compEvent, context.getLastLocation());
                    } else
                        temporarilyStageEvent(event, compEvent, null);
                    if (event.getEventType() == EventType.EVT_DROP
                            || event.getEventType() == EventType.EVT_DISCONNECT
                            || event.getEventType() == EventType.SIP_DROP
                            || event.getEventType() == EventType.SIP_DISCONNECT
                            || event.getEventType() == EventType.EVT_UNANSWERED
                            || event.getEventType() == EventType.SIP_UNANSWERED
                            || event.getEventType() == EventType.EVT_CALLFAIL
                            || event.getEventType() == EventType.SIP_CALLFAIL) {
                        unstageAndUploadEvent(compEvent, event);
                    }
                } catch (Exception ex) {
                    LoggerUtil.logToFile(LoggerUtil.Level.ERROR, TAG, "runEvent",
                            "Exception occured starting event " + eventType.name(), ex);
                    return;
                }
            }
        });
    }

    /**
     * When a new piece of data is received (like signal strength) it usually associates
     * itself with the currently staged event. This is achieved by publicizing the id
     * of the currently staged event through this method.
     * @return The ID (in the local sqlite database) of the currently staged event
     */
    public long getStagedEventId() {
        try {
            if (stagedEvent == null) {
                return -1;
            } else {
                return Long.parseLong(stagedEvent.getUri().getLastPathSegment());
            }
        } catch (Exception ex) {
        }
        return -1;
    }

    /**
     * Returns a reference to the ongoing events list.
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<EventCouple> getOngoingEventCouples() {
        return (List<EventCouple>) ((ArrayList<EventCouple>) this.ongoingEventCouples).clone();
    }

    public List<EventObj> getOngoingEvents() {
        return this.ongoingEvents;
    }

    public EventObj getLatestEvent() {
        if (this.ongoingEvents == null)
            return null;
        if (this.ongoingEvents.size() == 0)
            return null;
        return this.ongoingEvents.get(this.ongoingEvents.size() - 1);
    }

    /**
     * At the start of the event, stores like-timestamped entries in the tables for signals, cells and locations
     * This is so that querying the timespan of the event recording will easily return records for the beginning of the event
     * among other things
     * @param event
     */
    public void signalSnapshot(EventObj event) {
        // In case the signal hasn't changed recently (last 30 sec or so), 
        // the signal won't be retrieved by the cursor unless we timestamp the last known signal now
        try {

            TelephonyManager telephonyManager = (TelephonyManager) context
                    .getSystemService(Context.TELEPHONY_SERVICE);
            // Update the database and intents with a snapshot of the last known signal, cell, location
            SignalEx signal = context.getPhoneState().getLastMMCSignal();
            long timestamp = System.currentTimeMillis();
            if (event != null)
                timestamp = event.getEventTimestamp();
            if (signal != null) {
                signal.setTimestamp(timestamp);
                context.getPhoneState().clearLastMMCSignal(); // to force a duplicate signal to be added
                context.getPhoneStateListener().processNewMMCSignal(signal);
            }

            CellLocationEx cell = context.getPhoneStateListener().getLastCellLocation();
            if (cell != null) {
                cell.setCellIdTimestamp(timestamp);
                context.getPhoneStateListener().clearLastCellLocation();
                context.getPhoneStateListener().processNewCellLocation(cell);
            }

            if (event == null)
                return;

            ongoingEvents.add(event);
            Location location = context.getLastLocation();
            int satellites = context.getLastNumSatellites();
            if (location != null) {
                location.setTime(timestamp);
                context.setLastLocation(null); // to force a duplicate location to be added
                context.processNewFilteredLocation(location, satellites);
            }

            // set an initial location on the event if it is recent, but update it if a better location becomes available
            location = context.getLastLocation();
            event.setSignal(context.getPhoneState().getLastMMCSignal());
            event.setCell(context.getPhoneStateListener().getLastCellLocation());
            event.setServiceState(context.getPhoneState().getLastServiceStateObj());

            if (location != null && location.getTime() - 10000 > System.currentTimeMillis())
                event.setLocation(location, context.getLastNumSatellites());

            // Also, set the coverage flags at the time of the event
            int tier = context.getPhoneState().getNetworkGeneration();
            int servicestate = context.getPhoneState().getLastServiceState();
            int datastate = telephonyManager.getDataState();
            int activeConnection = PhoneState.ActiveConnection(context);
            if (activeConnection == 10)
                event.setFlag(EventObj.SERVICE_WIFI, true);
            else if (activeConnection == 11)
                event.setFlag(EventObj.SERVICE_WIMAX, true);
            ReportManager reportManager = ReportManager.getInstance(context);
            if (reportManager.manualPlottingEvent != null || event.getEventType() == EventType.MAN_PLOTTING)
                event.setFlag(EventObj.MANUAL_SAMPLES, true);

            if (datastate == TelephonyManager.DATA_CONNECTED || activeConnection > 1)
                event.setFlag(EventObj.SERVICE_DATA, true);
            else if (datastate == TelephonyManager.DATA_SUSPENDED && servicestate == ServiceState.STATE_IN_SERVICE) // not counted as data outage if data turned off
                event.setFlag(EventObj.SERVICE_DATA, true);
            else
                event.setFlag(EventObj.SERVICE_DATA, false);
            if (servicestate == ServiceState.STATE_OUT_OF_SERVICE
                    || servicestate == ServiceState.STATE_EMERGENCY_ONLY)
                event.setFlag(EventObj.SERVICE_VOICE, false);
            else
                event.setFlag(EventObj.SERVICE_VOICE, true);
            if (tier > 2)
                event.setFlag(EventObj.SERVICE_3G, true);
            if (tier > 4)
                event.setFlag(EventObj.SERVICE_4G, true);
            if (context.getPhoneState().isCallConnected() || context.getPhoneState().isCallDialing()
                    || event.getEventType().getIntValue() <= 6)
                event.setFlag(EventObj.PHONE_INUSE, true);

            // dropped call detection support using logcat or precise call state?
            if (event.getEventType().getIntValue() <= 7) {
                String pname = context.getPackageName();
                int permissionForReadLogs = context.getPackageManager()
                        .checkPermission(android.Manifest.permission.READ_LOGS, pname); //0 means allowed
                int permissionForPrecise = context.getPackageManager()
                        .checkPermission("android.permission.READ_PRECISE_PHONE_STATE", pname); // 0 means allowed
                if (permissionForPrecise == 0)
                    event.setFlag(EventObj.CALL_PRECISE, true);
                else if (permissionForReadLogs == 0)
                    event.setFlag(EventObj.CALL_LOGCAT, true);
            }

            event.setBattery(DeviceInfoOld.battery);
            if (event.getEventType() == EventType.COV_VOD_NO || event.getEventType() == EventType.COV_VOD_YES
                    || event.getEventType() == EventType.COV_UPDATE
                    || event.getEventType() == EventType.TRAVEL_CHECK) {
                long duration = 0;
                if (mLastServiceStateChangeTimeStamp != 0) {
                    duration = System.currentTimeMillis() - mLastServiceStateChangeTimeStamp;
                    mLastServiceStateChangeTimeStamp = System.currentTimeMillis();
                    event.setDuration(duration);
                } else
                    duration = mLastScreenOffDuration;

            }
            if (event.getEventType() == EventType.COV_3G_NO || event.getEventType() == EventType.COV_3G_YES
                    || event.getEventType() == EventType.COV_UPDATE
                    || event.getEventType() == EventType.TRAVEL_CHECK) {
                long duration = 0;
                if (mLast3GStateChangeTimeStamp != 0) {
                    duration = System.currentTimeMillis() - mLast3GStateChangeTimeStamp;
                    if (event.getEventType() == EventType.COV_UPDATE
                            || event.getEventType() == EventType.TRAVEL_CHECK)
                        event.setEventIndex(duration);
                    else
                        event.setDuration(duration);
                    mLast3GStateChangeTimeStamp = System.currentTimeMillis();
                }
            }
            //context.getCellHistory ().snapshotHistory();
            String conn = context.getConnectionHistory().updateConnectionHistory(telephonyManager.getNetworkType(),
                    telephonyManager.getDataState(), telephonyManager.getDataActivity(),
                    context.getPhoneState().getLastServiceStateObj(),
                    context.getConnectivityManager().getActiveNetworkInfo());
            context.getIntentDispatcher().updateConnection(conn, true);

            WifiInfo wifiinfo = getWifiInfo();
            WifiConfiguration wifiConfig = getWifiConfig();
            event.setWifi(wifiinfo, wifiConfig);

            localReportEvent(event);
        } catch (Exception e) {
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "signalSnapshot", "error", e);
        }
    }

    private boolean bScreenIsOn = true;

    /*
     * Prevent contributing to coverage % durations when screen is off
     * Android doesnt properly detect outages when screen is off, so we dont want to count it as being in or out of coverage
     * When screen is turned off for more than 2 minutes, set timer = 0 so events send duration = 0
     * When screen is turned back on, set the current time, so events send durations back to the time screen was turned on
     */
    public void screenChanged(boolean bScreenOn) {
        bScreenIsOn = bScreenOn;
        // If screen turns off and remains off for 2 minutes, and not on phone call, stop counting duration
        if (bScreenOn == false && !context.getPhoneState().isCallConnected()) {
            context.handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    // If screen is still off after 2 minutes, clear timer
                    if (bScreenIsOn == false) {
                        if (mLastServiceStateChangeTimeStamp != 0)
                            mLastScreenOffDuration = System.currentTimeMillis() - mLastServiceStateChangeTimeStamp;
                        mLast3GStateChangeTimeStamp = 0;
                        mLastServiceStateChangeTimeStamp = 0;
                    }
                }
            }, 120000);
            if (context.isMMCActive() && (isEventRunning(EventType.MAN_TRACKING)
                    || (isEventRunning(EventType.EVT_FILLIN) && context.getUsageLimits().getUsageProfile() > 2)))
                context.keepAwake(true, false); // Force the screen back on if it turned off while coverage tracking
        } else // if screen is on, and times were cleared because of screen off, start duration time again
        {
            if (mLastServiceStateChangeTimeStamp == 0)
                mLastServiceStateChangeTimeStamp = System.currentTimeMillis();
            if (mLast3GStateChangeTimeStamp == 0)
                mLast3GStateChangeTimeStamp = System.currentTimeMillis();
        }
    }

    public void dumpEvents() {
        for (EventCouple eventCouple : ongoingEventCouples) {
            if (eventCouple.getStopEvent() != null) {
                temporarilyStageEvent(eventCouple.getStopEvent(), eventCouple.getStartEvent(), null);
            }
        }
    }

    public void localReportEvent(EventObj event) {
        EventUploader uploader = new EventUploader(event, null, context, true);
        uploader.report(true, null);
    }

    /**
     * This method temporarily stages the method so that incoming data (like signal strength) would be able 
     * to associate itself with the staged event. The complimentary event is also stored alongside.
     * @param event
     * @param complimentaryEvent
     */
    public void temporarilyStageEvent(EventObj event, EventObj complimentaryEvent, Location location) {

        int satellites = 0;
        if (context.getGpsManager() != null && location != null) {
            satellites = context.getGpsManager().getNumberOfSatellites();
        }
        if (event.getStageTimestamp() > 0) // already staged
            return;

        if (location != null)
            event.setLocation(location, satellites);
        // If a connect event didnt get a good location before call disconnected, update its location
        if (complimentaryEvent != null
                && complimentaryEvent.getEventType().getGenre() == EventTypeGenre.START_OF_COUPLE
                && location != null && location.getAccuracy() < 100
                && location.getTime() - 120000 < complimentaryEvent.getEventTimestamp()
                && (complimentaryEvent.getLocation() == null
                        || complimentaryEvent.getLocation().getAccuracy() > 100))
            complimentaryEvent.setLocation(location, satellites);
        else if (complimentaryEvent != null
                && complimentaryEvent.getEventType().getGenre() == EventTypeGenre.START_OF_COUPLE
                && location != null && location.getTime() - 120000 < complimentaryEvent.getEventTimestamp()
                && complimentaryEvent.getLocation() == null)
            complimentaryEvent.setLocation(location, satellites);

        // start a an uploader for local reporting at the first fix 
        if (!event.getEventType().waitsForSpeed())// != EventType.MAN_SPEEDTEST && event.getEventType() != EventType.APP_MONITORING)
        {
            EventUploader uploader = new EventUploader(event, null, context, true);
            uploader.report(true, location);
        }
        // If Manual tracking (drive test) is in progress, immediately upload the new event on first fix, and resume the tracking
        if (stagedEvent != null && stagedEvent.getEventType() == EventType.MAN_TRACKING
                && event.getEventType() != EventType.MAN_TRACKING) {
            unstageAndUploadEvent(event, complimentaryEvent);
            return;
        } else if (event.getEventType().waitsForSpeed() && (event.getDownloadSpeed() != 0 || location == null)) {
            //if (stagedEvent == null)
            //   stagedEvent = event;
            unstageAndUploadEvent(event, null);
            return;
        }
        //now unstage the previously staged event (if any).
        else if (stagedEvent != null) {
            unstageAndUploadEvent(stagedEvent, stagedComplimentaryEvent);
        }

        //first disable the timer as it will be reset soon (if necessary)
        if (stageTimerTask != null) {
            stageTimerTask.cancel();
        }
        //now stage the new event and its complimentary
        stagedEvent = event;
        stagedComplimentaryEvent = complimentaryEvent;

        if (event.getEventType().getPostEventStageTime() == 0 || !context.isServiceRunning()
                || event.gpsListener == null) {
            unstageAndUploadEvent(event, complimentaryEvent);
            stagedEvent = null;
            stagedComplimentaryEvent = null;
            return;
        }
        //now set the timer to schedule the newly-staged-event's un-staging
        stageTimerTask = new StageTimerTask(event, complimentaryEvent);
        int stageTime = event.getEventType().getPostEventStageTime();

        if ((event.getEventType() == EventType.EVT_CONNECT || event.getEventType() == EventType.SIP_CONNECT)
                && context.getUsageLimits().getUsageProfile() == 0)
            stageTime = 60000;
        if ((event.getEventType() == EventType.EVT_CONNECT || event.getEventType() == EventType.SIP_CONNECT)
                && context.getUsageLimits().getUsageProfile() == 1)
            stageTime = 900000;
        if (EventObj.isDisabledEvent(context, EventObj.DISABLE_LONGCALL)
                && (event.getEventType() == EventType.EVT_CONNECT || event.getEventType() == EventType.SIP_CONNECT))
            stageTime = 30000;
        stageTimer.schedule(stageTimerTask, stageTime);

        //MMCLogger.logToFile(MMCLogger.Level.DEBUG, TAG, "temporarilyStageEvent", "staged event type=" + event.getEventType().getEventName() + " id=" + event.getUri().getLastPathSegment());
    }

    /**
     * This method uses the {@link #eventCache} to check whether the event is currently running.
     * @param startEventType
     * @return
     */
    public boolean isEventRunning(EventType startEventType) {
        return eventCache.contains(startEventType);
    }

    public boolean isEventRunning(int iEventType) {
        EventType eventType = EventType.get(iEventType);
        return eventCache.contains(eventType);
    }

    /**
     * This method uses the {@link #eventCache} to check whether the event is currently running.
     */
    public boolean areEventsRunning() {
        return !eventCache.isEmpty();
    }

    /**
     * Returns the event couple with the matching start event type and stop event type.
     * @param startEventType
     * @param stopEventType
     * @return
     */
    public EventCouple getEventCouple(EventType startEventType, EventType stopEventType) {
        for (EventCouple item : ongoingEventCouples) {
            if (item.getStartEventType() == startEventType && item.getStopEventType() == stopEventType)
                return item;
        }
        return null;
    }

    /**
     * This method "un-stages" the currently staged event and then attempts to upload that event
     * to the MMC server. Although, if the staged event is null, then this method just returns.
     */
    public EventObj unstageAndUploadEvent(EventObj event, EventObj complimentaryEvent) {
        try {
            {
                if (event == null) {
                    LoggerUtil.logToFile(LoggerUtil.Level.ERROR, "EventManager", "unstageAndUploadEvent",
                            "stagedEvent=null");
                    return null;
                }

                // Just don't remove a start-of-couple event until the end-of-couple is removed
                if (event.getEventType().getGenre() == EventTypeGenre.SINGLETON
                        || event.getEventType().getGenre() == EventTypeGenre.END_OF_COUPLE) {
                    eventCache.remove(event.getEventType());
                }

                if (ongoingEvents.contains(event))
                    ongoingEvents.remove(event);

                //MMCLogger.logToFile(MMCLogger.Level.DEBUG, "EventManager", "unstageAndUploadEvent", "event=" + event.getEventType() );
                event.setStageTimestamp(System.currentTimeMillis());
                event.setFlag(EventObj.SERVER_READY, true);
                MainService.getGpsManager().unregisterListener(event.gpsListener);
                // dont upload an event if in roaming

                final EventUploader uploader = new EventUploader(event, complimentaryEvent, context, false);
                if (uploader.event != null) {
                    context.uploadingEvent(true);
                    if (context.isServiceRunning()) {
                        new Thread((Runnable) uploader, EventUploader.class.getSimpleName()).start();
                    } else
                        uploader.run();
                }
                if (stagedEvent == event)
                    stagedEvent = null;
                if (stagedComplimentaryEvent == complimentaryEvent)
                    stagedComplimentaryEvent = null;

            }
        } catch (Exception e) {
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "unstageAndUploadEvent", "error", e);
        }
        return event;
    }

    /**
     * This method "un-stages" the currently staged event, so it stops capturing data
     * received from the listeners, and removes it from the list of currently running events.
     * This does not upload the event.
     */
    public void unstageEvent(EventObj event) {

        if (stagedEvent == event)
            stagedEvent = null;
        if (event == null)
            return;
        event.setStageTimestampToNow();
        MainService.getGpsManager().unregisterListener(event.gpsListener);
        eventCache.remove(event.getEventType());
        if (ongoingEvents.contains(event))
            ongoingEvents.remove(event);
    }

    public void cancelCouple(EventCouple cancelCouple) {
        ongoingEventCouples.remove(cancelCouple);
    }

    /**
     * This is the timer task that the stage timer invokes (when necessary) to "un-stage"
     * an event. In addition, this task also removes the event from the {@link #eventCache}
     * and attempts to upload the event to the MMC Server.
     */
    class StageTimerTask extends TimerTask {
        EventObj thisevent, complimentaryEvent;

        public StageTimerTask(EventObj evt, EventObj cevt) {
            thisevent = evt;
            complimentaryEvent = cevt;
        }

        @Override
        public void run() {

            EventObj event = unstageAndUploadEvent(thisevent, complimentaryEvent);

        }
    }

    /**
     * This is the timer task that the upload timer invokes to upload the event to the server
     * using the provided {@link EventUploader}.
     * @author Abhin
     *
     */

    class UploadTimerTask extends TimerTask {
        private EventUploader eventUploader;

        public UploadTimerTask(EventUploader _eventUploader) {
            this.eventUploader = _eventUploader;
        }

        @Override
        public void run() {
            new Thread((Runnable) eventUploader, UploadTimerTask.class.getSimpleName()).run();
        }
    }

    public void sendQueuedEvents() {
        // run an event uploader thread with a null event
        // this will simply check the queue and send any events now eligable to be sent
        EventUploader uploader = new EventUploader(null, null, context, false);
        context.uploadingEvent(true);
        uploadTimer.schedule(new UploadTimerTask(uploader), 20000);
    }

    public static ConcurrentLinkedQueue<EventDataEnvelope> getEventQueue() {
        return eventQueue;
    }

    public static void setEventQueue(ConcurrentLinkedQueue<EventDataEnvelope> eventQueue) {
        EventManager.eventQueue = eventQueue;
    }

    public WifiInfo getWifiInfo() {
        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();

        return wifiInfo;
    }

    public WifiConfiguration getWifiConfig() {
        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        WifiConfiguration activeConfig = null;
        if (wifiManager.getConfiguredNetworks() == null)
            return null;

        for (WifiConfiguration conn : wifiManager.getConfiguredNetworks()) {
            if ((conn.BSSID != null && conn.BSSID.equals(wifiInfo.getBSSID()))
                    || (conn.SSID != null && conn.SSID.equals(wifiInfo.getSSID()))) {
                activeConfig = conn;
                break;
            }
        }
        if (activeConfig != null) {
            return activeConfig;
        }
        return null;
    }

    class ActiveTestItem {
        public int trigger;
        public EventType eventType;

        public ActiveTestItem(EventType _eventType, int _trigger) {
            eventType = _eventType;
            trigger = _trigger;
        }
    }

    private ConcurrentLinkedQueue<ActiveTestItem> mTestQueue = new ConcurrentLinkedQueue<ActiveTestItem>();
    private Thread testQueueThread = null;
    private static CountDownLatch testingLatch = null;

    public void setSpeedtestUrls(String downloadUrl, String uploadUrl, String latencyUrl) {
        mSpeedTestTrigger.setCarrierDownloadUrl(downloadUrl);
        mSpeedTestTrigger.setCarrierUploadUrl(uploadUrl);
        mSpeedTestTrigger.setCarrierLatencyUrl(latencyUrl);
    }

    public void handleSMSDeliverNotification(int smsID, Long deliveryTime) {
        mSpeedTestTrigger.handleSMSDeliveryNotification(deliveryTime, smsID);
    }

    public void receivedSMSTestReply(JSONObject smsMsg) throws JSONException {
        try {
            String smsID = smsMsg.getString("smsID");
            String smsServerSentTime = smsMsg.getString("serverSendTime");
            long deliveredTime = PreferenceManager.getDefaultSharedPreferences(context)
                    .getLong(PreferenceKeys.Miscellaneous.SMS_DELIVERY_TIME, 0);
            PreferenceManager.getDefaultSharedPreferences(context).edit()
                    .putLong(PreferenceKeys.Miscellaneous.SMS_DELIVERY_TIME, 0).commit();

            mSpeedTestTrigger.handleSMSReply(smsID, "0", smsServerSentTime, String.valueOf(deliveredTime));
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    public boolean queueActiveTest(EventType eventType, int trigger) {
        if (!QosAPI.isEventEnabled(context, eventType) || !QosAPI.isEventPermitted(context, eventType, trigger))
            return false; // Event has not been enabled by server

        // If no test is in progress, run now
        // Add test test to the queue, waiting for tests in progress
        if (mTestQueue.size() < 12 && context.getPhoneState().isCallConnected() == false
                && context.getPhoneState().isCallDialing() == false)
            mTestQueue.add(new ActiveTestItem(eventType, trigger));
        else {
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest", "skipping active test");
            return false;
        }
        // Ensure one worker thread to run each test in the queue, then the thread ends when empty
        if (testQueueThread == null) {
            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest", "begin worker thread");
            testQueueThread = new Thread() {
                public void run() {
                    while (!mTestQueue.isEmpty()) {
                        if (testingLatch != null)
                            try {
                                boolean res = testingLatch.await(500, TimeUnit.SECONDS);
                            } catch (InterruptedException e) {
                                LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest",
                                        "testingLatch InterruptedException", e);
                            }
                        testingLatch = null;
                        ActiveTestItem item = mTestQueue.poll();

                        if (item.trigger == 2 && context.isInTracking() == false) {
                            LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest",
                                    "drive test expired before test completed:" + item.eventType.toString());
                            continue;
                        }
                        triggerActiveTest(item.eventType, item.trigger);
                        LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest",
                                "trigger test from queue:" + item.eventType.toString());
                    }
                    testQueueThread = null;
                    LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "queueActiveTest", "end worker thread");
                }
            };
            testQueueThread.start();
        }
        return true;
    }

    // Each test must call this when finsihed to allow next test to start
    public void setActiveTestComplete(EventType evtType) {

        if (testingLatch != null) {
            testingLatch.countDown();
        }

        if (mTestQueue.isEmpty() && context.getTrackingManager().isAdvancedTrackingWaiting()) {

            context.getTrackingManager().runAdvancedTrackingTests();
        }

        LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "setActiveTestComplete",
                "test=" + evtType.getIntValue() + ", latch=" + testingLatch);

    }

    /**
     * 'Update' or Check-In Event called every few hours
     * @param isCheckin
     * @param isStartup
     */
    public void triggerUpdateEvent(boolean isCheckin, boolean isStartup) {
        EventObj event;

        if (!context.getResources().getBoolean(R.bool.ALLOW_CHECK_INS) && isCheckin)
            event = new EventObj(EventType.COV_UPDATE);
        else
            event = triggerSingletonEvent(EventType.COV_UPDATE);

        event.isCheckin = isCheckin;
        LoggerUtil.logToFile(LoggerUtil.Level.DEBUG, TAG, "triggerUpdateEvent", "starting ");
        String reason = "";
        if (isCheckin) {
            reason = "periodic 3 hour check in";
            context.pruneDB();
            context.getCellHistory().clearCellHistory();
            context.getConnectionHistory().clearHistory();
        } else if (isStartup)
            reason = "checking in on startup";
        else
            reason = "user triggered update";
        //if (!isStartup)
        //   context.verifyRegistration ();
        //String apikey = context.getApiKey(context);

        PreferenceManager.getDefaultSharedPreferences(context).edit()
                .putLong(PreferenceKeys.Miscellaneous.LAST_TIME, System.currentTimeMillis()).commit();

    }

    private void triggerActiveTest(final EventType eventType, final int trigger) {
        testingLatch = new CountDownLatch(1);
        // trigger the test on the main thread and it will run a background task
        context.handler.post(new Runnable() {
            // @Override
            public void run() {

                if (eventType == EventType.EVT_VQ_CALL)
                    context.getVQManager().runTest(trigger);
                else if (eventType == EventType.MAN_SPEEDTEST)
                    mSpeedTestTrigger.runTest(true, trigger, eventType);
                else if (eventType == EventType.SMS_TEST)
                    mSpeedTestTrigger.runTest(true, trigger, eventType);
                else if (eventType == EventType.LATENCY_TEST)
                    mSpeedTestTrigger.runTest(false, trigger, eventType);
                else if (eventType == EventType.VIDEO_TEST)
                    mVideoTestTrigger.runTest(true, trigger, eventType);
                else if (eventType == EventType.AUDIO_TEST)
                    mVideoTestTrigger.runTest(true, trigger, eventType);
                else if (eventType == EventType.WEBPAGE_TEST)
                    mVideoTestTrigger.runTest(true, trigger, eventType);
                else if (eventType == EventType.YOUTUBE_TEST)
                    mVideoTestTrigger.runTest(true, trigger, eventType);
            }
        });
    }

    public void triggerSpeedTest(int trigger) {
        mSpeedTestTrigger.runTest(true, trigger, EventType.MAN_SPEEDTEST);
    }

    public void triggerSMSTest(int trigger) {
        mSpeedTestTrigger.runTest(true, trigger, EventType.SMS_TEST);
    }

    public void triggerFillinEvent() {
        triggerSingletonEvent(EventType.EVT_FILLIN);
    }

    public String runLatencyTest(boolean background) {
        String latency = "-1";
        try {
            //         latency =  String.valueOf(mSpeedTestTrigger.updateTestLatency());
            int trigger = 0;
            if (background == true) {
                String reason = null;
                int allow = PreferenceManager.getDefaultSharedPreferences(context)
                        .getInt(PreferenceKeys.Miscellaneous.AUTO_CONNECTION_TESTS, 1);

                if (PhoneState.isNetworkWifi(context))
                    reason = "on wifi";
                if (!context.isOnline())
                    reason = "offline";
                if (allow != 1 && !LoggerUtil.isDebuggable())
                    reason = "not enabled";
                if (context.getPhoneState().isCallConnected() || context.getPhoneState().isCallDialing() == true)
                    reason = "phone call";
                if (context.getUsageLimits().getUsageProfile() <= UsageLimits.MINIMAL)
                    reason = "minimal";
                if (context.getUsageLimits().getDormantMode() >= 1)
                    reason = "dormant";
                if (EventObj.isDisabledEvent(context, EventObj.DISABLE_AUTOCONNTEST))
                    reason = "disableevent";
                if (reason != null) {
                    LoggerUtil.logToFile(LoggerUtil.Level.ERROR, TAG, "runLatencyTest cancelled ", reason);
                    return reason;
                }

                trigger = 1;
            }
            final int ftrigger = trigger;
            context.handler.post(new Runnable() {
                // @Override
                public void run() {
                    mSpeedTestTrigger.runTest(false, ftrigger, EventType.LATENCY_TEST);
                }
            });
        } catch (Exception e) {
            latency = e.toString();
        }
        return latency;
    }

    public void killSpeedTest() {
        LoggerUtil.logToFile(LoggerUtil.Level.ERROR, TAG, "killSpeedTest", "calling mSpeedTestTrigger.killTest()");
        mSpeedTestTrigger.killTest();
    }

    public void killActiveTest(int testType) {
        LoggerUtil.logToFile(LoggerUtil.Level.ERROR, TAG, "killVideoTest", "calling mVideoTestTrigger.killTest()");
        mVideoTestTrigger.killTest(testType);
    }

    public UsageLimits getUsageLimits() {
        return Global.usageLimits;
    }

    /**
     * This class encapsulates the data and the logic for gps management for the {@link MainService} service.
     * This class is intended to be used for turning on the gps when an event takes place.
     */
    public class GpsListenerForEvent extends GpsListener {
        public static final int MIN_SATELLITE_COUNT_FOR_FF_TIMEOUT = 3;
        public static final int MIN_BATTERY_LEVEL_FOR_FF_TIMEOUT = 20; //in percentage
        public static final int DEFAULT_FF_TIMEOUT_EXTENSION = 90000; //in milliseconds
        public EventObj thisEvent;
        public EventObj complimentaryEvent;
        private long tmNeighborUpdate = 0;

        public GpsListenerForEvent(EventObj event, EventObj compEvent) {
            super("GpsListenerForEvent");
            this.thisEvent = event;
            this.complimentaryEvent = compEvent;

            //now adjust the operationTimeout for the GpsListener
            //if (compEvent == null || compEvent.getUri() == null){
            int stageTime = event.getEventType().getPostEventStageTime();
            if (event.getEventType() == EventType.EVT_CONNECT || event.getEventType() == EventType.SIP_CONNECT) {
                if (EventObj.isDisabledEvent(context, EventObj.DISABLE_LONGCALL))
                    stageTime = 30000;
                else if (getUsageLimits().getUsageProfile() == 0)
                    stageTime = 60000;
                else if (getUsageLimits().getUsageProfile() == 1)
                    stageTime = 600000;

                this.setFirstFixTimeout(stageTime);
            }
            if (getUsageLimits().exceededGps(event.getEventType(), true)) {
                this.setFirstFixTimeout(20000);
            }

        }

        /**
         * If the number of satellites is greater than or equal to {@value #MIN_SATELLITE_COUNT_FOR_FF_TIMEOUT}
         * and if the battery level is above {@value #MIN_BATTERY_LEVEL_FOR_FF_TIMEOUT} percent, then renew the
         * timeout by {@value #DEFAULT_FF_TIMEOUT_EXTENSION} milliseconds.
         */
        @Override
        public int attemptToRenewFirstFixTimeout(int numberOfSatellites) {
            if (numberOfSatellites > MIN_SATELLITE_COUNT_FOR_FF_TIMEOUT && DeviceInfoOld.battery > 20) {
                if (this.getFirstFixTimeout() <= 30000)
                    return 30000;
                return DEFAULT_FF_TIMEOUT_EXTENSION;
            }
            return 0;
        }

        /**
         * For events, the chaining property of the GpsManager is not utilised. Instead, we rely on the timeout
         * to stop the gps for us. Therefore, after processing the location, we simply return true.
         */
        @Override
        public boolean onLocationUpdate(Location location, int satellites) {

            if (location != null)
                this.setLastLocation(location);

            //lastSatellites = getGpsManager().getNumberOfSatellites();
            if (location != null && location.getAccuracy() < GpsListener.LOCATION_UPDATE_MIN_EVENT_ACCURACY) // We'll settle for 75 meters to go in the database, but only <45 goes in the trend string
            {
                // Poor accuracy GPS will be stored in the table, but event will only use it if no accurate fix was obtained, and it wont go in trend
                context.setLastLocation(location);
                context.setLastSatellites(satellites);

            }
            // abort initial travelling event if last location is close to this location
            if (thisEvent.getEventType() == EventType.TRAVEL_CHECK) {
                if (context.getTravelDetector().confirmTravelling(location)) {
                    // This might trigger a drive test
                    if (context.getTravelDetector().isConfirmed() && context.getDriveTestTrigger().equals("travel")
                            && !context.isInTracking()) {
                        context.triggerDriveTest("travel", true);
                    }
                } else {
                    unstageEvent(thisEvent);
                    return false;
                }
            }
            if (location != null)
                stageEventIfNeeded(location);

            // Can we force extra neighbor list updates by requesting more Cell Locations?
            //         if (tmNeighborUpdate + 15000 < System.currentTimeMillis())
            //         {
            //            CellLocation.requestLocationUpdate();
            //            //cellHistory.updateNeighborHistory (null, null);
            //            tmNeighborUpdate = System.currentTimeMillis();
            //         }
            return true;
        }

        /**
         * Even if a timeout occurs, it is important that the event be staged and sent to the server.
         */
        @Override
        public void onTimeout() {
            {
                //we don't have a location, but stage the event regardless
                thisEvent.gpsListener = null;
                stageEventIfNeeded(null);
                getUsageLimits().addGpsFails();
                //MMCLogger.logToFile(MMCLogger.Level.DEBUG, "GpsListenerForEvent", "onTimeout", "startingEvent type=" + startingEvent.getEventType() + " id=" + startingEvent.getUri().getLastPathSegment() + "endingEvent " + (endingEvent==null ? "null" : "id=" + endingEvent.getEventType() + " id=" + (endingEvent.getUri()==null ? "null" : endingEvent.getUri().getLastPathSegment())));
            }
        }

        private void stageEventIfNeeded(Location location) {

            // Sfaegaurd. if any event other than phone connect is still runnign after 10 minutes, kill by unstaging it
            if (thisEvent.getEventTimestamp() + 10 * 60000 < System.currentTimeMillis()
                    && thisEvent.getEventType() != EventType.EVT_CONNECT
                    && thisEvent.getEventType() != EventType.SIP_CONNECT) {
                LoggerUtil.logToFile(LoggerUtil.Level.ERROR, "GpsListenerForEvent",
                        "KILLED event running longer than 10 minutes", "event=" + thisEvent.getEventType());
                unstageEvent(thisEvent);
                return;
            }

            // If this is a speed test, return unless ready with a download speed and a good enough location
            if (thisEvent.getEventType().waitsForSpeed())
                if ((thisEvent.getDownloadSpeed() == 0 && thisEvent.getLatency() == 0
                        && thisEvent.getDuration() == 0) || (location != null && location.getAccuracy() > 100))
                    return;
                //         //if the location exists but is not good enough, then don't stage the event just yet
                else if (location != null
                        && location.getAccuracy() > GpsListener.LOCATION_UPDATE_MIN_TREND_ACCURACY)
                    return; //staging is not needed just yet

            if (thisEvent.getStageTimestamp() == -1L) {
                //thisEvent.setStageTimestampToNow();
                temporarilyStageEvent(thisEvent, complimentaryEvent, location);
                thisEvent.setStageTimestampToNow();
                // When a travel_check has been confirmed, might run a latency test too
                if (thisEvent.getEventType() == EventType.TRAVEL_CHECK && location != null) {
                    runLatencyTest(true);
                }
            }

        }

        @Override
        public void gpsStarted() {
            context.clearLastGoodLocation(); // clear last known location when the gps starts, so that if the gps fails to get a fix, we know it failed afterward
            context.updateNumberOfSatellites(0, 0);
        }

        @Override
        public void gpsStopped() {
        }

        public EventObj getEvent() {
            return thisEvent;
        }
    }

    /**
     * This class encapsulates the data and the logic for gps management for the {@link MainService} service.
     * This class is intended to be used for turning on the gps when an event takes place.
     */
    class LocationListenerForEvent extends GpsListener {
        EventObj event = null;

        public LocationListenerForEvent(EventObj _event) {
            super("LocationListenerForEvent");
            setProvider(LocationManager.NETWORK_PROVIDER);
            this.setFirstFixRenewalAllowed(false);
            this.setFirstFixTimeout(30000);
            event = _event;
        }

        /**
         * Store the coarse location in the location database
         */
        @Override
        public boolean onLocationUpdate(Location location, int satellites) {
            // Don't mistake this for a good GPS fix for an event, it often understates the network location error
            // Prevents the location from travel detection or fill-ins
            if (location != null && location.getAccuracy() < GpsListener.LOCATION_UPDATE_MIN_EVENT_ACCURACY)
                location.setAccuracy(GpsListener.LOCATION_UPDATE_MIN_EVENT_ACCURACY + 1);
            if (location != null && location.getAccuracy() == 0)
                location.setAccuracy(1);
            context.setLastLocation(location);
            location.setTime(System.currentTimeMillis());
            context.setLastSatellites(satellites);
            // Poor accuracy GPS will be stored in the table, but event will only use it if no accurate fix was obtained, and it wont go in trend
            ContentValues values = ContentValuesGenerator.generateFromLocation(location, 0, satellites);
            context.getDBProvider().insert(TablesEnum.LOCATIONS.getContentUri(), values);

            if (event.getLocation() == null && location != null)
                event.setLocation(location, satellites);

            if (event.getLocalID() > 0 && location != null) {
                context.getReportManager().updateEventField(event.getLocalID(),
                        LocalStorageReporter.Events.KEY_LATITUDE, String.valueOf(location.getLatitude()));
                context.getReportManager().updateEventField(event.getLocalID(),
                        LocalStorageReporter.Events.KEY_LONGITUDE, String.valueOf(location.getLongitude()));
            }

            return false; // stop listening after the first network location
        }

        @Override
        public void gpsStarted() {
        }

        @Override
        public void gpsStopped() {
        }
    }
}