Now uses service to grab departure estimates (in preparation for a notification service that will share that EtdService)
This commit is contained in:
parent
64d5eda3b5
commit
7a4e8da4f0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
/bin
|
||||
/gen
|
@ -2,14 +2,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.dougkeen.bart"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="20"
|
||||
android:versionName="2.0.2" >
|
||||
android:versionCode="21"
|
||||
android:versionName="2.1.0" >
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="7"
|
||||
android:minSdkVersion="8"
|
||||
android:targetSdkVersion="14" />
|
||||
|
||||
<application
|
||||
@ -62,6 +62,9 @@
|
||||
android:name=".data.BartContentProvider"
|
||||
android:authorities="com.dougkeen.bart.dataprovider"
|
||||
android:label="BartRunner data provider" />
|
||||
|
||||
<service android:name=".NotificationService" />
|
||||
<service android:name=".EtdService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="#000"
|
||||
android:paddingBottom="10dip"
|
||||
android:paddingLeft="5dip"
|
||||
android:paddingRight="5dip"
|
||||
android:paddingTop="10dip"
|
||||
android:singleLine="true" >
|
||||
|
||||
</TextView>
|
@ -1,7 +1,6 @@
|
||||
package com.dougkeen.bart;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
@ -36,7 +35,7 @@ public class AddRouteDialogFragment extends AbstractRouteSelectionFragment {
|
||||
values.put(RoutesColumns.FROM_STATION.string, origin.abbreviation);
|
||||
values.put(RoutesColumns.TO_STATION.string, destination.abbreviation);
|
||||
|
||||
Uri newUri = getActivity().getContentResolver().insert(
|
||||
getActivity().getContentResolver().insert(
|
||||
Constants.FAVORITE_CONTENT_URI, values);
|
||||
|
||||
if (((CheckBox) getDialog().findViewById(R.id.return_checkbox))
|
||||
|
@ -72,7 +72,14 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
|
||||
.findViewById(R.id.trainLengthText);
|
||||
initTextSwitcher(textSwitcher);
|
||||
|
||||
textSwitcher.setCurrentText(departure.getTrainLengthText());
|
||||
final String estimatedArrivalTimeText = departure
|
||||
.getEstimatedArrivalTimeText(getContext());
|
||||
if (!StringUtils.isBlank(estimatedArrivalTimeText)) {
|
||||
textSwitcher.setCurrentText("Est. arrival "
|
||||
+ estimatedArrivalTimeText);
|
||||
} else {
|
||||
textSwitcher.setCurrentText(departure.getTrainLengthText());
|
||||
}
|
||||
textSwitcher.setTextProvider(new TextProvider() {
|
||||
@Override
|
||||
public String getText(long tickNumber) {
|
||||
|
580
src/com/dougkeen/bart/EtdService.java
Normal file
580
src/com/dougkeen/bart/EtdService.java
Normal file
@ -0,0 +1,580 @@
|
||||
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 void scheduleDepartureFetch(int millisUntilExecute) {
|
||||
mPendingEtdRequest = true;
|
||||
mRunnableQueue.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
fetchLatestDepartures();
|
||||
}
|
||||
}, millisUntilExecute);
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package com.dougkeen.bart;
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.DialogInterface;
|
||||
@ -18,13 +17,11 @@ import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
import android.widget.SimpleCursorAdapter.ViewBinder;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.WazaBe.HoloEverywhere.AlertDialog;
|
||||
import com.WazaBe.HoloEverywhere.AlertDialog.Builder;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.ActionMode;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
@ -40,8 +37,6 @@ public class RoutesListActivity extends SherlockFragmentActivity {
|
||||
private static final TimeZone PACIFIC_TIME = TimeZone
|
||||
.getTimeZone("America/Los_Angeles");
|
||||
|
||||
private static final int DIALOG_DELETE_ROUTE = 0;
|
||||
|
||||
protected Cursor mQuery;
|
||||
|
||||
private Uri mCurrentlySelectedUri;
|
||||
|
@ -2,19 +2,21 @@ package com.dougkeen.bart;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
@ -31,51 +33,43 @@ import com.actionbarsherlock.view.ActionMode;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuInflater;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.dougkeen.bart.EtdService.EtdServiceBinder;
|
||||
import com.dougkeen.bart.EtdService.EtdServiceListener;
|
||||
import com.dougkeen.bart.controls.CountdownTextView;
|
||||
import com.dougkeen.bart.controls.Ticker;
|
||||
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.model.TextProvider;
|
||||
import com.dougkeen.bart.networktasks.GetRealTimeDeparturesTask;
|
||||
import com.dougkeen.bart.networktasks.GetScheduleInformationTask;
|
||||
|
||||
public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
public class ViewDeparturesActivity extends SherlockFragmentActivity implements
|
||||
EtdServiceListener {
|
||||
|
||||
private static final int UNCERTAINTY_THRESHOLD = 17;
|
||||
private static final int LOADER_ID = 123;
|
||||
|
||||
private Uri mUri;
|
||||
|
||||
private Station mOrigin;
|
||||
private Station mDestination;
|
||||
private int mAverageTripLength;
|
||||
private int mAverageTripSampleCount;
|
||||
|
||||
private Departure mSelectedDeparture;
|
||||
private Departure mBoardedDeparture;
|
||||
|
||||
private DepartureArrayAdapter mDeparturesAdapter;
|
||||
|
||||
private ScheduleInformation mLatestScheduleInfo;
|
||||
|
||||
private TextView mEmptyView;
|
||||
private ProgressBar mProgress;
|
||||
|
||||
private AsyncTask<StationPair, Integer, RealTimeDepartures> mGetDeparturesTask;
|
||||
private AsyncTask<StationPair, Integer, ScheduleInformation> mGetScheduleInformationTask;
|
||||
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
||||
private boolean mDepartureFetchIsPending;
|
||||
private boolean mScheduleFetchIsPending;
|
||||
|
||||
private ActionMode mActionMode;
|
||||
|
||||
private EtdService mEtdService;
|
||||
|
||||
private boolean mBound = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -89,23 +83,56 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
mUri = intent.getData();
|
||||
}
|
||||
|
||||
Cursor cursor = managedQuery(mUri, new String[] {
|
||||
RoutesColumns.FROM_STATION.string,
|
||||
RoutesColumns.TO_STATION.string,
|
||||
RoutesColumns.AVERAGE_TRIP_LENGTH.string,
|
||||
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string }, null, null,
|
||||
null);
|
||||
final Uri uri = mUri;
|
||||
|
||||
if (!cursor.moveToFirst()) {
|
||||
throw new IllegalStateException("URI not found: " + mUri.toString());
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey("origin")
|
||||
&& savedInstanceState.containsKey("destination")) {
|
||||
mOrigin = Station.getByAbbreviation(savedInstanceState
|
||||
.getString("origin"));
|
||||
mDestination = Station.getByAbbreviation(savedInstanceState
|
||||
.getString("destination"));
|
||||
} else {
|
||||
getSupportLoaderManager().initLoader(LOADER_ID, null,
|
||||
new LoaderCallbacks<Cursor>() {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new CursorLoader(
|
||||
ViewDeparturesActivity.this, uri,
|
||||
new String[] {
|
||||
RoutesColumns.FROM_STATION.string,
|
||||
RoutesColumns.TO_STATION.string },
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader,
|
||||
Cursor cursor) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
Log.wtf(Constants.TAG,
|
||||
"Couldn't find Route record for the current Activity");
|
||||
}
|
||||
mOrigin = Station.getByAbbreviation(cursor
|
||||
.getString(0));
|
||||
mDestination = Station.getByAbbreviation(cursor
|
||||
.getString(1));
|
||||
cursor.close();
|
||||
((TextView) findViewById(R.id.listTitle))
|
||||
.setText(mOrigin.name + " to "
|
||||
+ mDestination.name);
|
||||
if (mBound && mEtdService != null)
|
||||
mEtdService
|
||||
.registerListener(ViewDeparturesActivity.this);
|
||||
|
||||
getSupportLoaderManager().destroyLoader(LOADER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
mOrigin = Station.getByAbbreviation(cursor.getString(0));
|
||||
mDestination = Station.getByAbbreviation(cursor.getString(1));
|
||||
mAverageTripLength = cursor.getInt(2);
|
||||
mAverageTripSampleCount = cursor.getInt(3);
|
||||
|
||||
((TextView) findViewById(R.id.listTitle)).setText(mOrigin.name + " to "
|
||||
+ mDestination.name);
|
||||
|
||||
mEmptyView = (TextView) findViewById(android.R.id.empty);
|
||||
mEmptyView.setText(R.string.departure_wait_message);
|
||||
@ -115,13 +142,12 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
mDeparturesAdapter = new DepartureArrayAdapter(this,
|
||||
R.layout.departure_listing);
|
||||
if (savedInstanceState != null) {
|
||||
|
||||
if (savedInstanceState.containsKey("departures")) {
|
||||
for (Parcelable departure : savedInstanceState
|
||||
.getParcelableArray("departures")) {
|
||||
mDeparturesAdapter.add((Departure) departure);
|
||||
mDeparturesAdapter.notifyDataSetChanged();
|
||||
}
|
||||
mDeparturesAdapter.notifyDataSetChanged();
|
||||
}
|
||||
if (savedInstanceState.containsKey("boardedDeparture")) {
|
||||
mBoardedDeparture = (Departure) savedInstanceState
|
||||
@ -165,39 +191,40 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
return (AdapterView<ListAdapter>) findViewById(android.R.id.list);
|
||||
}
|
||||
|
||||
private DepartureArrayAdapter mListAdapter;
|
||||
private final ServiceConnection mConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mEtdService = null;
|
||||
mBound = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mEtdService = ((EtdServiceBinder) service).getService();
|
||||
mBound = true;
|
||||
if (getStationPair() != null) {
|
||||
mEtdService.registerListener(ViewDeparturesActivity.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected DepartureArrayAdapter getListAdapter() {
|
||||
return mListAdapter;
|
||||
return mDeparturesAdapter;
|
||||
}
|
||||
|
||||
protected void setListAdapter(DepartureArrayAdapter adapter) {
|
||||
mListAdapter = adapter;
|
||||
getListView().setAdapter(mListAdapter);
|
||||
mDeparturesAdapter = adapter;
|
||||
getListView().setAdapter(mDeparturesAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
cancelDataFetch();
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (mEtdService != null)
|
||||
mEtdService.unregisterListener(this);
|
||||
if (mBound)
|
||||
unbindService(mConnection);
|
||||
Ticker.getInstance().stopTicking();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
cancelDataFetch();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void cancelDataFetch() {
|
||||
if (mGetDeparturesTask != null) {
|
||||
mGetDeparturesTask.cancel(true);
|
||||
mDepartureFetchIsPending = false;
|
||||
}
|
||||
if (mGetScheduleInformationTask != null) {
|
||||
mGetScheduleInformationTask.cancel(true);
|
||||
mScheduleFetchIsPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -211,11 +238,15 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
outState.putParcelable("boardedDeparture", mBoardedDeparture);
|
||||
outState.putParcelable("selectedDeparture", mSelectedDeparture);
|
||||
outState.putBoolean("hasActionMode", mActionMode != null);
|
||||
outState.putString("origin", mOrigin.abbreviation);
|
||||
outState.putString("destination", mDestination.abbreviation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
bindService(new Intent(this, EtdService.class), mConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
Ticker.getInstance().startTicking();
|
||||
}
|
||||
|
||||
@ -223,9 +254,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
if (!mDepartureFetchIsPending) {
|
||||
fetchLatestDepartures();
|
||||
}
|
||||
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
mWakeLock = powerManager
|
||||
.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK,
|
||||
@ -237,381 +265,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mIgnoreDepartureDirection = false;
|
||||
|
||||
private void fetchLatestDepartures() {
|
||||
if (!hasWindowFocus())
|
||||
return;
|
||||
if (mGetDeparturesTask != null
|
||||
&& mGetDeparturesTask.getStatus().equals(
|
||||
AsyncTask.Status.RUNNING)) {
|
||||
// Don't overlap fetches
|
||||
return;
|
||||
}
|
||||
|
||||
mGetDeparturesTask = new GetRealTimeDeparturesTask(
|
||||
mIgnoreDepartureDirection) {
|
||||
@Override
|
||||
public void onResult(RealTimeDepartures result) {
|
||||
mDepartureFetchIsPending = false;
|
||||
Log.i(Constants.TAG, "Processing data from server");
|
||||
mProgress.setVisibility(View.INVISIBLE);
|
||||
processLatestDepartures(result);
|
||||
Log.i(Constants.TAG, "Done processing data from server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
mDepartureFetchIsPending = false;
|
||||
Log.w(Constants.TAG, e.getMessage(), e);
|
||||
Toast.makeText(ViewDeparturesActivity.this,
|
||||
R.string.could_not_connect, Toast.LENGTH_LONG).show();
|
||||
mEmptyView.setText(R.string.could_not_connect);
|
||||
mProgress.setVisibility(View.INVISIBLE);
|
||||
// Try again in 60s
|
||||
scheduleDepartureFetch(60000);
|
||||
}
|
||||
};
|
||||
Log.i(Constants.TAG, "Fetching data from server");
|
||||
mGetDeparturesTask.execute(new StationPair(mOrigin, mDestination));
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
}
|
||||
|
||||
private void fetchLatestSchedule() {
|
||||
if (!hasWindowFocus())
|
||||
return;
|
||||
if (mGetScheduleInformationTask != null
|
||||
&& mGetScheduleInformationTask.getStatus().equals(
|
||||
AsyncTask.Status.RUNNING)) {
|
||||
// Don't overlap fetches
|
||||
return;
|
||||
}
|
||||
|
||||
mGetScheduleInformationTask = new GetScheduleInformationTask() {
|
||||
@Override
|
||||
public void onResult(ScheduleInformation result) {
|
||||
mScheduleFetchIsPending = false;
|
||||
Log.i(Constants.TAG, "Processing data from server");
|
||||
mLatestScheduleInfo = result;
|
||||
applyScheduleInformation();
|
||||
Log.i(Constants.TAG, "Done processing data from server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
mScheduleFetchIsPending = false;
|
||||
Log.w(Constants.TAG, e.getMessage(), e);
|
||||
Toast.makeText(ViewDeparturesActivity.this,
|
||||
R.string.could_not_connect, Toast.LENGTH_LONG).show();
|
||||
mEmptyView.setText(R.string.could_not_connect);
|
||||
mProgress.setVisibility(View.GONE);
|
||||
|
||||
// Try again in 60s
|
||||
scheduleScheduleInfoFetch(60000);
|
||||
}
|
||||
};
|
||||
Log.i(Constants.TAG, "Fetching data from server");
|
||||
mGetScheduleInformationTask.execute(new StationPair(mOrigin,
|
||||
mDestination));
|
||||
}
|
||||
|
||||
protected void processLatestDepartures(RealTimeDepartures result) {
|
||||
if (result.getDepartures().isEmpty()) {
|
||||
result.includeTransferRoutes();
|
||||
}
|
||||
if (result.getDepartures().isEmpty()) {
|
||||
result.includeDoubleTransferRoutes();
|
||||
}
|
||||
if (result.getDepartures().isEmpty() && !mIgnoreDepartureDirection) {
|
||||
// Let's try again, ignoring direction (this sometimes comes up when
|
||||
// you travel between Millbrae and SFO)
|
||||
mIgnoreDepartureDirection = true;
|
||||
scheduleDepartureFetch(50);
|
||||
return;
|
||||
}
|
||||
if (result.getDepartures().isEmpty()) {
|
||||
final TextView textView = mEmptyView;
|
||||
textView.setText(R.string.no_data_message);
|
||||
mProgress.setVisibility(View.GONE);
|
||||
Linkify.addLinks(textView, Linkify.WEB_URLS);
|
||||
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 (mDeparturesAdapter.isEmpty()) {
|
||||
// Just copy everything to the adapter
|
||||
for (Departure departure : departures) {
|
||||
if (firstDeparture == null) {
|
||||
firstDeparture = departure;
|
||||
}
|
||||
mDeparturesAdapter.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 adapter
|
||||
int adapterIndex = -1;
|
||||
for (Departure departure : departures) {
|
||||
adapterIndex++;
|
||||
Departure existingDeparture = null;
|
||||
if (adapterIndex < mDeparturesAdapter.getCount()) {
|
||||
existingDeparture = mDeparturesAdapter
|
||||
.getItem(adapterIndex);
|
||||
}
|
||||
// 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
|
||||
mDeparturesAdapter.remove(existingDeparture);
|
||||
if (adapterIndex < mDeparturesAdapter.getCount()) {
|
||||
// Try again with next departure (keep in mind the next
|
||||
// departure is now at the current index, since we
|
||||
// removed a member)
|
||||
existingDeparture = mDeparturesAdapter
|
||||
.getItem(adapterIndex);
|
||||
} else {
|
||||
// Reached the end of the adapter... 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 {
|
||||
mDeparturesAdapter.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
mDeparturesAdapter.notifyDataSetChanged();
|
||||
requestScheduleIfNecessary();
|
||||
|
||||
if (hasWindowFocus() && 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 (mDeparturesAdapter.getCount() == 0) {
|
||||
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 = mDeparturesAdapter.getItem(mDeparturesAdapter
|
||||
.getCount() - 1);
|
||||
if (mLatestScheduleInfo.getLatestDepartureTime() < lastDeparture
|
||||
.getMeanEstimate()) {
|
||||
fetchLatestSchedule();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyScheduleInformation() {
|
||||
int localAverageLength = mLatestScheduleInfo.getAverageTripLength();
|
||||
|
||||
int departuresCount = mDeparturesAdapter.getCount();
|
||||
|
||||
// Let's get smallest interval between departures
|
||||
int smallestDepartureInterval = 0;
|
||||
long previousDepartureTime = 0;
|
||||
for (int departureIndex = 0; departureIndex < departuresCount; departureIndex++) {
|
||||
Departure departure = mDeparturesAdapter.getItem(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 = mDeparturesAdapter.getItem(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) {
|
||||
mDeparturesAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// 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 scheduleDepartureFetch(int millisUntilExecute) {
|
||||
if (!mDepartureFetchIsPending) {
|
||||
postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
fetchLatestDepartures();
|
||||
}
|
||||
}, millisUntilExecute);
|
||||
mDepartureFetchIsPending = true;
|
||||
Log.i(Constants.TAG, "Scheduled another departure fetch in "
|
||||
+ millisUntilExecute / 1000 + "s");
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleScheduleInfoFetch(int millisUntilExecute) {
|
||||
if (!mScheduleFetchIsPending) {
|
||||
postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
fetchLatestSchedule();
|
||||
}
|
||||
}, millisUntilExecute);
|
||||
mScheduleFetchIsPending = true;
|
||||
Log.i(Constants.TAG, "Scheduled another schedule fetch in "
|
||||
+ millisUntilExecute / 1000 + "s");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean postDelayed(Runnable runnable, long delayMillis) {
|
||||
return mEmptyView.postDelayed(runnable, delayMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getSupportMenuInflater();
|
||||
@ -744,4 +397,105 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onETDChanged(final List<Departure> departures) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (departures.isEmpty()) {
|
||||
final TextView textView = mEmptyView;
|
||||
textView.setText(R.string.no_data_message);
|
||||
mProgress.setVisibility(View.INVISIBLE);
|
||||
Linkify.addLinks(textView, Linkify.WEB_URLS);
|
||||
} else {
|
||||
// Merge lists
|
||||
if (mDeparturesAdapter.getCount() > 0) {
|
||||
int adapterIndex = -1;
|
||||
for (Departure departure : departures) {
|
||||
adapterIndex++;
|
||||
Departure existingDeparture = null;
|
||||
if (adapterIndex < mDeparturesAdapter.getCount()) {
|
||||
existingDeparture = mDeparturesAdapter
|
||||
.getItem(adapterIndex);
|
||||
}
|
||||
while (existingDeparture != null
|
||||
&& !departure.equals(existingDeparture)) {
|
||||
mDeparturesAdapter.remove(existingDeparture);
|
||||
if (adapterIndex < mDeparturesAdapter
|
||||
.getCount()) {
|
||||
existingDeparture = mDeparturesAdapter
|
||||
.getItem(adapterIndex);
|
||||
} else {
|
||||
existingDeparture = null;
|
||||
}
|
||||
}
|
||||
if (existingDeparture != null) {
|
||||
existingDeparture.mergeEstimate(departure);
|
||||
} else {
|
||||
mDeparturesAdapter.add(departure);
|
||||
existingDeparture = departure;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final DepartureArrayAdapter listAdapter = getListAdapter();
|
||||
listAdapter.clear();
|
||||
// addAll() method isn't available until API level 11
|
||||
for (Departure departure : departures) {
|
||||
listAdapter.add(departure);
|
||||
}
|
||||
}
|
||||
|
||||
if (mBoardedDeparture != null) {
|
||||
for (Departure departure : departures) {
|
||||
if (departure.equals(mBoardedDeparture)) {
|
||||
mBoardedDeparture = departure;
|
||||
refreshBoardedDeparture();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
getListAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final String errorMessage) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(ViewDeparturesActivity.this, errorMessage,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestStarted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestEnded() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mProgress.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public StationPair getStationPair() {
|
||||
if (mOrigin == null || mDestination == null)
|
||||
return null;
|
||||
return new StationPair(mOrigin, mDestination);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.dougkeen.bart;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
|
||||
@ -8,7 +7,6 @@ import com.actionbarsherlock.app.SherlockActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuInflater;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.dougkeen.bart.model.Constants;
|
||||
|
||||
public class ViewMapActivity extends SherlockActivity {
|
||||
|
||||
|
@ -46,7 +46,13 @@ public class TimedTextSwitcher extends TextSwitcher implements
|
||||
Ticker.getInstance().addSubscriber(this);
|
||||
}
|
||||
|
||||
private String mLastText;
|
||||
private CharSequence mLastText;
|
||||
|
||||
@Override
|
||||
public void setCurrentText(CharSequence text) {
|
||||
mLastText = text;
|
||||
super.setCurrentText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick(long tickNumber) {
|
||||
|
@ -2,8 +2,6 @@ package com.dougkeen.bart.data;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.dougkeen.bart.model.Constants;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
@ -17,6 +15,8 @@ import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.dougkeen.bart.model.Constants;
|
||||
|
||||
public class BartContentProvider extends ContentProvider {
|
||||
|
||||
private static final UriMatcher sUriMatcher;
|
||||
@ -95,14 +95,27 @@ public class BartContentProvider extends ContentProvider {
|
||||
int match = sUriMatcher.match(uri);
|
||||
|
||||
if (match == ARBITRARY_ROUTE) {
|
||||
final String origin = uri.getPathSegments().get(1);
|
||||
final String destination = uri.getPathSegments().get(2);
|
||||
|
||||
qb.setTables(DatabaseHelper.FAVORITES_TABLE_NAME);
|
||||
qb.setProjectionMap(sFavoritesProjectionMap);
|
||||
qb.appendWhere(String.format("%s = '%s' AND %s = '%s'",
|
||||
RoutesColumns.FROM_STATION, origin,
|
||||
RoutesColumns.TO_STATION, destination));
|
||||
Cursor query = qb.query(db, projection, selection, selectionArgs,
|
||||
null, null, sortOrder);
|
||||
if (query.getCount() > 0)
|
||||
return query;
|
||||
|
||||
MatrixCursor returnCursor = new MatrixCursor(projection);
|
||||
RowBuilder newRow = returnCursor.newRow();
|
||||
|
||||
for (String column : projection) {
|
||||
if (column.equals(RoutesColumns.FROM_STATION.string)) {
|
||||
newRow.add(uri.getPathSegments().get(1));
|
||||
newRow.add(origin);
|
||||
} else if (column.equals(RoutesColumns.TO_STATION.string)) {
|
||||
newRow.add(uri.getPathSegments().get(2));
|
||||
newRow.add(destination);
|
||||
} else {
|
||||
newRow.add(null);
|
||||
}
|
||||
@ -204,6 +217,24 @@ public class BartContentProvider extends ContentProvider {
|
||||
+ ')' : ""), whereArgs);
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return count;
|
||||
} else if (match == ARBITRARY_ROUTE) {
|
||||
// Get the route with the origin and destination provided, and
|
||||
// simply delegate to the previous log branch. If the given route
|
||||
// doesn't exist, do nothing.
|
||||
String origin = uri.getPathSegments().get(1);
|
||||
String destination = uri.getPathSegments().get(2);
|
||||
|
||||
Cursor query = db.query(DatabaseHelper.FAVORITES_TABLE_NAME,
|
||||
new String[] { RoutesColumns._ID.string },
|
||||
RoutesColumns.FROM_STATION.string + "=? AND "
|
||||
+ RoutesColumns.TO_STATION.string + "=?",
|
||||
new String[] { origin, destination }, null, null, null);
|
||||
|
||||
if (query.moveToFirst()) {
|
||||
return update(ContentUris.withAppendedId(
|
||||
Constants.FAVORITE_CONTENT_URI, query.getLong(0)),
|
||||
values, where, whereArgs);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -302,11 +302,17 @@ public class Departure implements Parcelable, Comparable<Departure> {
|
||||
setMinEstimate(newMin);
|
||||
setMaxEstimate(newMax);
|
||||
}
|
||||
|
||||
if (!hasAnyArrivalEstimate() && departure.hasAnyArrivalEstimate()) {
|
||||
setArrivalTimeOverride(departure.getArrivalTimeOverride());
|
||||
setEstimatedTripTime(departure.getEstimatedTripTime());
|
||||
}
|
||||
}
|
||||
|
||||
public int compareTo(Departure another) {
|
||||
return (this.getMinutes() > another.getMinutes()) ? 1 : ((this
|
||||
.getMinutes() == another.getMinutes()) ? 0 : -1);
|
||||
return (this.getMeanSecondsLeft() > another.getMeanSecondsLeft()) ? 1
|
||||
: ((this.getMeanSecondsLeft() == another.getMeanSecondsLeft()) ? 0
|
||||
: -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -117,7 +117,8 @@ public class Route {
|
||||
} else if (routeLine.transferLine2 != null
|
||||
&& viaLine.equals(routeLine.transferLine2)) {
|
||||
return true;
|
||||
} else if (requiresTransfer && transferLines != null && !transferLines.isEmpty()) {
|
||||
} else if (requiresTransfer && transferLines != null
|
||||
&& !transferLines.isEmpty()) {
|
||||
return transferLines.contains(viaLine);
|
||||
} else {
|
||||
int originIndex = viaLine.stations.indexOf(origin);
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.dougkeen.bart.model;
|
||||
|
||||
|
||||
public class StationPair {
|
||||
public StationPair(Station origin, Station destination) {
|
||||
super();
|
||||
@ -18,4 +17,36 @@ public class StationPair {
|
||||
public Station getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public boolean isBetweenStations(Station station1, Station station2) {
|
||||
return (origin.equals(station1) && destination.equals(station2))
|
||||
|| (origin.equals(station2) && destination.equals(station1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result
|
||||
+ ((destination == null) ? 0 : destination.hashCode());
|
||||
result = prime * result + ((origin == null) ? 0 : origin.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
StationPair other = (StationPair) obj;
|
||||
if (destination != other.destination)
|
||||
return false;
|
||||
if (origin != other.origin)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package com.dougkeen.bart.networktasks;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user