BartRunnerAndroid/src/com/dougkeen/bart/EtdService.java

592 lines
18 KiB
Java

package com.dougkeen.bart;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.commons.lang3.math.NumberUtils;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.RealTimeDepartures;
import com.dougkeen.bart.model.ScheduleInformation;
import com.dougkeen.bart.model.ScheduleItem;
import com.dougkeen.bart.model.Station;
import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.networktasks.GetRealTimeDeparturesTask;
import com.dougkeen.bart.networktasks.GetScheduleInformationTask;
public class EtdService extends Service {
private IBinder mBinder;
private Map<StationPair, EtdServiceEngine> mServiceEngineMap;
public EtdService() {
super();
mBinder = new EtdServiceBinder();
mServiceEngineMap = new HashMap<StationPair, EtdServiceEngine>();
}
public void registerListener(EtdServiceListener listener) {
StationPair stationPair = getStationPairFromListener(listener);
if (stationPair == null)
return;
if (!mServiceEngineMap.containsKey(stationPair)) {
mServiceEngineMap.put(stationPair,
new EtdServiceEngine(stationPair));
}
mServiceEngineMap.get(stationPair).registerListener(listener);
}
private StationPair getStationPairFromListener(EtdServiceListener listener) {
StationPair route = listener.getStationPair();
if (route == null) {
Log.wtf(Constants.TAG,
"Somehow we got a listener that's returning a null route O_o");
}
return route;
}
public void unregisterListener(EtdServiceListener listener) {
StationPair stationPair = getStationPairFromListener(listener);
if (stationPair == null)
return;
if (mServiceEngineMap.containsKey(stationPair)) {
mServiceEngineMap.get(stationPair).unregisterListener(listener);
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public interface EtdServiceListener {
void onETDChanged(final List<Departure> departures);
void onError(String errorMessage);
void onRequestStarted();
void onRequestEnded();
StationPair getStationPair();
}
public class EtdServiceBinder extends Binder {
public EtdService getService() {
return EtdService.this;
}
}
private class EtdServiceEngine {
private static final int UNCERTAINTY_THRESHOLD = 17;
private Uri mUri;
private final StationPair mStationPair;
private boolean mIgnoreDepartureDirection = false;
private boolean mPendingEtdRequest = false;
private int mAverageTripLength;
private int mAverageTripSampleCount;
// We'll only use the keys
private WeakHashMap<EtdServiceListener, Boolean> mListeners;
private List<Departure> mLatestDepartures;
private ScheduleInformation mLatestScheduleInfo;
private AsyncTask<StationPair, Integer, RealTimeDepartures> mGetDeparturesTask;
private AsyncTask<StationPair, Integer, ScheduleInformation> mGetScheduleInformationTask;
private Handler mRunnableQueue;
private boolean mStarted = false;
public EtdServiceEngine(final StationPair route) {
mStationPair = route;
mListeners = new WeakHashMap<EtdService.EtdServiceListener, Boolean>();
mRunnableQueue = new Handler();
mLatestDepartures = new ArrayList<Departure>();
mUri = Constants.ARBITRARY_ROUTE_CONTENT_URI_ROOT.buildUpon()
.appendPath(mStationPair.getOrigin().abbreviation)
.appendPath(mStationPair.getDestination().abbreviation)
.build();
Cursor cursor = new CursorLoader(EtdService.this, mUri,
new String[] { RoutesColumns.AVERAGE_TRIP_LENGTH.string,
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string },
null, null, null).loadInBackground();
if (cursor.moveToFirst()) {
mAverageTripLength = cursor.getInt(0);
mAverageTripSampleCount = cursor.getInt(1);
}
cursor.close();
}
protected void registerListener(EtdServiceListener listener) {
mListeners.put(listener, true);
if (!mPendingEtdRequest) {
mStarted = true;
fetchLatestDepartures();
}
}
protected void unregisterListener(EtdServiceListener listener) {
mListeners.remove(listener);
if (mListeners.isEmpty()) {
if (mGetDeparturesTask != null
&& mGetDeparturesTask.getStatus().equals(
AsyncTask.Status.RUNNING)) {
mGetDeparturesTask.cancel(true);
}
if (mGetScheduleInformationTask != null
&& mGetScheduleInformationTask.getStatus().equals(
AsyncTask.Status.RUNNING)) {
mGetScheduleInformationTask.cancel(true);
}
mStarted = false;
}
}
private void notifyListenersOfETDChange() {
for (EtdServiceListener listener : mListeners.keySet()) {
listener.onETDChanged(mLatestDepartures);
}
}
private void notifyListenersOfError(String errorMessage) {
for (EtdServiceListener listener : mListeners.keySet()) {
listener.onError(errorMessage);
}
}
private void notifyListenersOfRequestStart() {
for (EtdServiceListener listener : mListeners.keySet()) {
listener.onRequestStarted();
}
}
private void notifyListenersOfRequestEnd() {
for (EtdServiceListener listener : mListeners.keySet()) {
listener.onRequestEnded();
}
}
private void fetchLatestDepartures() {
if (mGetDeparturesTask != null
&& mGetDeparturesTask.equals(AsyncTask.Status.RUNNING)) {
// Don't overlap fetches
return;
}
if (!mStarted)
return;
GetRealTimeDeparturesTask task = new GetRealTimeDeparturesTask(
mIgnoreDepartureDirection) {
@Override
public void onResult(RealTimeDepartures result) {
Log.d(Constants.TAG, "Processing data from server");
processLatestDepartures(result);
Log.d(Constants.TAG, "Done processing data from server");
notifyListenersOfRequestEnd();
mPendingEtdRequest = false;
}
@Override
public void onError(Exception e) {
Log.w(Constants.TAG, e.getMessage(), e);
notifyListenersOfError(getString(R.string.could_not_connect));
// Try again in 60s
scheduleDepartureFetch(60000);
notifyListenersOfRequestEnd();
}
};
mGetDeparturesTask = task;
Log.d(Constants.TAG, "Fetching data from server");
task.execute(new StationPair(mStationPair.getOrigin(), mStationPair
.getDestination()));
notifyListenersOfRequestStart();
}
private void fetchLatestSchedule() {
if (mGetScheduleInformationTask != null
&& mGetScheduleInformationTask.getStatus().equals(
AsyncTask.Status.RUNNING)) {
// Don't overlap fetches
return;
}
GetScheduleInformationTask task = new GetScheduleInformationTask() {
@Override
public void onResult(ScheduleInformation result) {
Log.d(Constants.TAG, "Processing data from server");
mLatestScheduleInfo = result;
applyScheduleInformation(result);
Log.d(Constants.TAG, "Done processing data from server");
}
@Override
public void onError(Exception e) {
Log.w(Constants.TAG, e.getMessage(), e);
notifyListenersOfError(getString(R.string.could_not_connect));
// Try again in 60s
scheduleScheduleInfoFetch(60000);
}
};
Log.i(Constants.TAG, "Fetching data from server");
mGetScheduleInformationTask = task;
task.execute(new StationPair(mStationPair.getOrigin(), mStationPair
.getDestination()));
}
protected void applyScheduleInformation(ScheduleInformation result) {
int localAverageLength = mLatestScheduleInfo.getAverageTripLength();
int departuresCount = mLatestDepartures.size();
// Let's get smallest interval between departures
int smallestDepartureInterval = 0;
long previousDepartureTime = 0;
for (int departureIndex = 0; departureIndex < departuresCount; departureIndex++) {
Departure departure = mLatestDepartures.get(departureIndex);
if (previousDepartureTime == 0) {
previousDepartureTime = departure.getMeanEstimate();
} else if (smallestDepartureInterval == 0) {
smallestDepartureInterval = (int) (departure
.getMeanEstimate() - previousDepartureTime);
} else {
smallestDepartureInterval = Math
.min(smallestDepartureInterval,
(int) (departure.getMeanEstimate() - previousDepartureTime));
}
}
// Match scheduled departures with real time departures in adapter
int lastSearchIndex = 0;
int tripCount = mLatestScheduleInfo.getTrips().size();
boolean departureUpdated = false;
Departure lastUnestimatedTransfer = null;
int departuresWithoutEstimates = 0;
for (int departureIndex = 0; departureIndex < departuresCount; departureIndex++) {
Departure departure = mLatestDepartures.get(departureIndex);
for (int i = lastSearchIndex; i < tripCount; i++) {
ScheduleItem trip = mLatestScheduleInfo.getTrips().get(i);
// Definitely not a match if they have different
// destinations
if (!departure.getDestination().abbreviation.equals(trip
.getTrainHeadStation())) {
continue;
}
long departTimeDiff = Math.abs(trip.getDepartureTime()
- departure.getMeanEstimate());
final long millisUntilTripDeparture = trip
.getDepartureTime() - System.currentTimeMillis();
final int equalityTolerance = (departure.getOrigin() != null) ? NumberUtils
.max(departure.getOrigin().departureEqualityTolerance,
ScheduleItem.SCHEDULE_ITEM_DEPARTURE_EQUALS_TOLERANCE,
smallestDepartureInterval)
: ScheduleItem.SCHEDULE_ITEM_DEPARTURE_EQUALS_TOLERANCE;
if (departure.getOrigin() != null
&& departure.getOrigin().longStationLinger
&& departure.hasDeparted()
&& millisUntilTripDeparture > 0
&& millisUntilTripDeparture < equalityTolerance) {
departure.setArrivalTimeOverride(trip.getArrivalTime());
lastSearchIndex = i;
departureUpdated = true;
if (lastUnestimatedTransfer != null) {
lastUnestimatedTransfer.setArrivalTimeOverride(trip
.getArrivalTime());
departuresWithoutEstimates--;
}
break;
} else if (departTimeDiff <= (equalityTolerance + departure
.getUncertaintySeconds() * 1000)
&& departure.getEstimatedTripTime() != trip
.getTripLength()
&& !(departure.getOrigin().longStationLinger && departure
.hasDeparted())) {
departure.setEstimatedTripTime(trip.getTripLength());
lastSearchIndex = i;
departureUpdated = true;
if (lastUnestimatedTransfer != null) {
lastUnestimatedTransfer.setArrivalTimeOverride(trip
.getArrivalTime());
departuresWithoutEstimates--;
}
break;
}
}
// Don't estimate for non-scheduled transfers
if (!departure.getRequiresTransfer()) {
if (!departure.hasEstimatedTripTime()
&& localAverageLength > 0) {
// Use the average we just calculated if available
departure.setEstimatedTripTime(localAverageLength);
} else if (!departure.hasEstimatedTripTime()) {
// Otherwise just assume the global average
departure.setEstimatedTripTime(mAverageTripLength);
}
} else if (departure.getRequiresTransfer()
&& !departure.hasAnyArrivalEstimate()) {
lastUnestimatedTransfer = departure;
}
if (!departure.hasAnyArrivalEstimate()) {
departuresWithoutEstimates++;
}
}
if (departureUpdated) {
notifyListenersOfETDChange();
}
// Update global average
if (mLatestScheduleInfo.getTripCountForAverage() > 0) {
int newAverageSampleCount = mAverageTripSampleCount
+ mLatestScheduleInfo.getTripCountForAverage();
int newAverage = (mAverageTripLength * mAverageTripSampleCount + localAverageLength
* mLatestScheduleInfo.getTripCountForAverage())
/ newAverageSampleCount;
ContentValues contentValues = new ContentValues();
contentValues.put(RoutesColumns.AVERAGE_TRIP_LENGTH.string,
newAverage);
contentValues.put(
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string,
newAverageSampleCount);
getContentResolver().update(mUri, contentValues, null, null);
}
/*
* If we still have some departures without estimates, try again
* later
*/
if (departuresWithoutEstimates > 0) {
scheduleScheduleInfoFetch(20000);
}
}
private void processLatestDepartures(RealTimeDepartures result) {
if (result.getDepartures().isEmpty()) {
result.includeTransferRoutes();
}
if (result.getDepartures().isEmpty()) {
result.includeDoubleTransferRoutes();
}
if (result.getDepartures().isEmpty()
&& mStationPair.isBetweenStations(Station.MLBR,
Station.SFIA)) {
/*
* Let's try again, ignoring direction (this sometimes comes up
* when you travel between Millbrae and SFO... sometimes you
* need to travel north and transfer, sometimes you can travel
* south for a direct line)
*/
mIgnoreDepartureDirection = true;
scheduleDepartureFetch(50);
return;
}
boolean needsBetterAccuracy = false;
/*
* Keep track of first departure, since we'll request another quick
* refresh if it has departed.
*/
Departure firstDeparture = null;
final List<Departure> departures = result.getDepartures();
if (mLatestDepartures.isEmpty()) {
// Just copy everything to the departure list
for (Departure departure : departures) {
if (firstDeparture == null) {
firstDeparture = departure;
}
mLatestDepartures.add(departure);
}
/*
* Since all the departures are new, we'll definitely need
* better accuracy
*/
needsBetterAccuracy = true;
} else {
/*
* Let's merge the latest departure list with the instance
* departure list
*/
int instanceListIndex = -1;
for (Departure departure : departures) {
instanceListIndex++;
Departure existingDeparture = null;
if (instanceListIndex < mLatestDepartures.size()) {
existingDeparture = mLatestDepartures
.get(instanceListIndex);
}
/*
* Looks for departures at the beginning of the adapter that
* aren't in the latest list of departures
*/
while (existingDeparture != null
&& !departure.equals(existingDeparture)) {
// Remove old departure
mLatestDepartures.remove(existingDeparture);
if (instanceListIndex < mLatestDepartures.size()) {
/*
* Try again with next departure (keep in mind the
* next departure is now at the current index, since
* we removed a member)
*/
existingDeparture = mLatestDepartures
.get(instanceListIndex);
} else {
// Reached the end of the list... give up
existingDeparture = null;
}
}
/*
* Merge the estimate if we found a matching departure,
* otherwise add a new one to the adapter
*/
if (existingDeparture != null) {
existingDeparture.mergeEstimate(departure);
} else {
mLatestDepartures.add(departure);
existingDeparture = departure;
}
// Set first departure
if (firstDeparture == null) {
firstDeparture = existingDeparture;
}
// Check if estimate is accurate enough
if (existingDeparture.getUncertaintySeconds() > UNCERTAINTY_THRESHOLD) {
needsBetterAccuracy = true;
}
}
}
Collections.sort(mLatestDepartures);
notifyListenersOfETDChange();
requestScheduleIfNecessary();
if (firstDeparture != null) {
if (needsBetterAccuracy || firstDeparture.hasDeparted()) {
// Get more data in 20s
scheduleDepartureFetch(20000);
} else {
/*
* Get more 90 seconds before next train arrives, right when
* next train arrives, or 3 minutes, whichever is sooner
*/
final int intervalUntilNextDeparture = firstDeparture
.getMinSecondsLeft() * 1000;
final int alternativeInterval = 3 * 60 * 1000;
int interval = intervalUntilNextDeparture;
if (intervalUntilNextDeparture > 95000
&& intervalUntilNextDeparture < alternativeInterval) {
interval = interval - 90 * 1000;
} else if (intervalUntilNextDeparture > alternativeInterval) {
interval = alternativeInterval;
}
if (interval < 0) {
interval = 20000;
}
scheduleDepartureFetch(interval);
}
}
}
private void requestScheduleIfNecessary() {
// Bail if there's nothing to match schedules to
if (mLatestDepartures.isEmpty()) {
return;
}
// Fetch if we don't have anything at all
if (mLatestScheduleInfo == null) {
fetchLatestSchedule();
return;
}
/*
* Otherwise, check if the latest departure doesn't have schedule
* info... if not, fetch
*/
Departure lastDeparture = mLatestDepartures.get(mLatestDepartures
.size() - 1);
if (mLatestScheduleInfo.getLatestDepartureTime() < lastDeparture
.getMeanEstimate()) {
fetchLatestSchedule();
return;
}
}
private long mNextFetchClockTime = 0;
private void scheduleDepartureFetch(int millisUntilExecute) {
mPendingEtdRequest = true;
long now = System.currentTimeMillis();
long requestedFetchTime = now + millisUntilExecute;
if (mNextFetchClockTime > now
&& mNextFetchClockTime < requestedFetchTime) {
Log.d(Constants.TAG,
"Did not schedule departure fetch, since one is already scheduled");
} else {
mRunnableQueue.postDelayed(new Runnable() {
public void run() {
fetchLatestDepartures();
}
}, millisUntilExecute);
mNextFetchClockTime = requestedFetchTime;
Log.d(Constants.TAG, "Scheduled another departure fetch in "
+ millisUntilExecute / 1000 + "s");
}
}
private void scheduleScheduleInfoFetch(int millisUntilExecute) {
mRunnableQueue.postDelayed(new Runnable() {
public void run() {
fetchLatestSchedule();
}
}, millisUntilExecute);
Log.d(Constants.TAG, "Scheduled another schedule fetch in "
+ millisUntilExecute / 1000 + "s");
}
}
}