Now uses service to grab departure estimates (in preparation for a notification service that will share that EtdService)

This commit is contained in:
Doug Keen 2012-09-11 15:49:36 -07:00
parent 64d5eda3b5
commit 7a4e8da4f0
15 changed files with 878 additions and 479 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/bin
/gen

View File

@ -2,14 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dougkeen.bart" package="com.dougkeen.bart"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="20" android:versionCode="21"
android:versionName="2.0.2" > android:versionName="2.1.0" >
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-sdk <uses-sdk
android:minSdkVersion="7" android:minSdkVersion="8"
android:targetSdkVersion="14" /> android:targetSdkVersion="14" />
<application <application
@ -62,6 +62,9 @@
android:name=".data.BartContentProvider" android:name=".data.BartContentProvider"
android:authorities="com.dougkeen.bart.dataprovider" android:authorities="com.dougkeen.bart.dataprovider"
android:label="BartRunner data provider" /> android:label="BartRunner data provider" />
<service android:name=".NotificationService" />
<service android:name=".EtdService" />
</application> </application>
</manifest> </manifest>

View File

@ -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>

View File

@ -1,7 +1,6 @@
package com.dougkeen.bart; package com.dougkeen.bart;
import android.content.ContentValues; import android.content.ContentValues;
import android.net.Uri;
import android.view.View; import android.view.View;
import android.widget.CheckBox; import android.widget.CheckBox;
@ -36,7 +35,7 @@ public class AddRouteDialogFragment extends AbstractRouteSelectionFragment {
values.put(RoutesColumns.FROM_STATION.string, origin.abbreviation); values.put(RoutesColumns.FROM_STATION.string, origin.abbreviation);
values.put(RoutesColumns.TO_STATION.string, destination.abbreviation); values.put(RoutesColumns.TO_STATION.string, destination.abbreviation);
Uri newUri = getActivity().getContentResolver().insert( getActivity().getContentResolver().insert(
Constants.FAVORITE_CONTENT_URI, values); Constants.FAVORITE_CONTENT_URI, values);
if (((CheckBox) getDialog().findViewById(R.id.return_checkbox)) if (((CheckBox) getDialog().findViewById(R.id.return_checkbox))

View File

@ -72,7 +72,14 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
.findViewById(R.id.trainLengthText); .findViewById(R.id.trainLengthText);
initTextSwitcher(textSwitcher); 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() { textSwitcher.setTextProvider(new TextProvider() {
@Override @Override
public String getText(long tickNumber) { public String getText(long tickNumber) {

View 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");
}
}
}

View File

@ -3,7 +3,6 @@ package com.dougkeen.bart;
import java.util.Calendar; import java.util.Calendar;
import java.util.TimeZone; import java.util.TimeZone;
import android.app.Dialog;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.DialogInterface; import android.content.DialogInterface;
@ -18,13 +17,11 @@ import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.CursorAdapter; import android.widget.CursorAdapter;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter; import android.widget.SimpleCursorAdapter;
import android.widget.SimpleCursorAdapter.ViewBinder; import android.widget.SimpleCursorAdapter.ViewBinder;
import android.widget.TextView; import android.widget.TextView;
import com.WazaBe.HoloEverywhere.AlertDialog; import com.WazaBe.HoloEverywhere.AlertDialog;
import com.WazaBe.HoloEverywhere.AlertDialog.Builder;
import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.Menu;
@ -40,8 +37,6 @@ public class RoutesListActivity extends SherlockFragmentActivity {
private static final TimeZone PACIFIC_TIME = TimeZone private static final TimeZone PACIFIC_TIME = TimeZone
.getTimeZone("America/Los_Angeles"); .getTimeZone("America/Los_Angeles");
private static final int DIALOG_DELETE_ROUTE = 0;
protected Cursor mQuery; protected Cursor mQuery;
private Uri mCurrentlySelectedUri; private Uri mCurrentlySelectedUri;

View File

@ -2,19 +2,21 @@ package com.dougkeen.bart;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.math.NumberUtils; import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.PowerManager; 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.format.DateFormat;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.Log; import android.util.Log;
@ -31,51 +33,43 @@ import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; 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.CountdownTextView;
import com.dougkeen.bart.controls.Ticker; import com.dougkeen.bart.controls.Ticker;
import com.dougkeen.bart.data.RoutesColumns; import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants; import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure; 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.Station;
import com.dougkeen.bart.model.StationPair; import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.model.TextProvider; 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 Uri mUri;
private Station mOrigin; private Station mOrigin;
private Station mDestination; private Station mDestination;
private int mAverageTripLength;
private int mAverageTripSampleCount;
private Departure mSelectedDeparture; private Departure mSelectedDeparture;
private Departure mBoardedDeparture; private Departure mBoardedDeparture;
private DepartureArrayAdapter mDeparturesAdapter; private DepartureArrayAdapter mDeparturesAdapter;
private ScheduleInformation mLatestScheduleInfo;
private TextView mEmptyView; private TextView mEmptyView;
private ProgressBar mProgress; private ProgressBar mProgress;
private AsyncTask<StationPair, Integer, RealTimeDepartures> mGetDeparturesTask;
private AsyncTask<StationPair, Integer, ScheduleInformation> mGetScheduleInformationTask;
private PowerManager.WakeLock mWakeLock; private PowerManager.WakeLock mWakeLock;
private boolean mDepartureFetchIsPending;
private boolean mScheduleFetchIsPending;
private ActionMode mActionMode; private ActionMode mActionMode;
private EtdService mEtdService;
private boolean mBound = false;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -89,23 +83,56 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
mUri = intent.getData(); mUri = intent.getData();
} }
Cursor cursor = managedQuery(mUri, new String[] { final Uri uri = mUri;
RoutesColumns.FROM_STATION.string,
RoutesColumns.TO_STATION.string,
RoutesColumns.AVERAGE_TRIP_LENGTH.string,
RoutesColumns.AVERAGE_TRIP_SAMPLE_COUNT.string }, null, null,
null);
if (!cursor.moveToFirst()) { if (savedInstanceState != null
throw new IllegalStateException("URI not found: " + mUri.toString()); && 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 = (TextView) findViewById(android.R.id.empty);
mEmptyView.setText(R.string.departure_wait_message); mEmptyView.setText(R.string.departure_wait_message);
@ -115,13 +142,12 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
mDeparturesAdapter = new DepartureArrayAdapter(this, mDeparturesAdapter = new DepartureArrayAdapter(this,
R.layout.departure_listing); R.layout.departure_listing);
if (savedInstanceState != null) { if (savedInstanceState != null) {
if (savedInstanceState.containsKey("departures")) { if (savedInstanceState.containsKey("departures")) {
for (Parcelable departure : savedInstanceState for (Parcelable departure : savedInstanceState
.getParcelableArray("departures")) { .getParcelableArray("departures")) {
mDeparturesAdapter.add((Departure) departure); mDeparturesAdapter.add((Departure) departure);
mDeparturesAdapter.notifyDataSetChanged();
} }
mDeparturesAdapter.notifyDataSetChanged();
} }
if (savedInstanceState.containsKey("boardedDeparture")) { if (savedInstanceState.containsKey("boardedDeparture")) {
mBoardedDeparture = (Departure) savedInstanceState mBoardedDeparture = (Departure) savedInstanceState
@ -165,39 +191,40 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
return (AdapterView<ListAdapter>) findViewById(android.R.id.list); 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() { protected DepartureArrayAdapter getListAdapter() {
return mListAdapter; return mDeparturesAdapter;
} }
protected void setListAdapter(DepartureArrayAdapter adapter) { protected void setListAdapter(DepartureArrayAdapter adapter) {
mListAdapter = adapter; mDeparturesAdapter = adapter;
getListView().setAdapter(mListAdapter); getListView().setAdapter(mDeparturesAdapter);
} }
@Override @Override
protected void onPause() { protected void onStop() {
cancelDataFetch(); super.onStop();
if (mEtdService != null)
mEtdService.unregisterListener(this);
if (mBound)
unbindService(mConnection);
Ticker.getInstance().stopTicking(); 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 @Override
@ -211,11 +238,15 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
outState.putParcelable("boardedDeparture", mBoardedDeparture); outState.putParcelable("boardedDeparture", mBoardedDeparture);
outState.putParcelable("selectedDeparture", mSelectedDeparture); outState.putParcelable("selectedDeparture", mSelectedDeparture);
outState.putBoolean("hasActionMode", mActionMode != null); outState.putBoolean("hasActionMode", mActionMode != null);
outState.putString("origin", mOrigin.abbreviation);
outState.putString("destination", mDestination.abbreviation);
} }
@Override @Override
protected void onResume() { protected void onStart() {
super.onResume(); super.onStart();
bindService(new Intent(this, EtdService.class), mConnection,
Context.BIND_AUTO_CREATE);
Ticker.getInstance().startTicking(); Ticker.getInstance().startTicking();
} }
@ -223,9 +254,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity {
public void onWindowFocusChanged(boolean hasFocus) { public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus); super.onWindowFocusChanged(hasFocus);
if (hasFocus) { if (hasFocus) {
if (!mDepartureFetchIsPending) {
fetchLatestDepartures();
}
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager mWakeLock = powerManager
.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, .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 @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getSupportMenuInflater(); 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);
}
} }

View File

@ -1,6 +1,5 @@
package com.dougkeen.bart; package com.dougkeen.bart;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.webkit.WebView; import android.webkit.WebView;
@ -8,7 +7,6 @@ import com.actionbarsherlock.app.SherlockActivity;
import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem;
import com.dougkeen.bart.model.Constants;
public class ViewMapActivity extends SherlockActivity { public class ViewMapActivity extends SherlockActivity {

View File

@ -46,7 +46,13 @@ public class TimedTextSwitcher extends TextSwitcher implements
Ticker.getInstance().addSubscriber(this); Ticker.getInstance().addSubscriber(this);
} }
private String mLastText; private CharSequence mLastText;
@Override
public void setCurrentText(CharSequence text) {
mLastText = text;
super.setCurrentText(text);
}
@Override @Override
public void onTick(long tickNumber) { public void onTick(long tickNumber) {

View File

@ -2,8 +2,6 @@ package com.dougkeen.bart.data;
import java.util.HashMap; import java.util.HashMap;
import com.dougkeen.bart.model.Constants;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
@ -17,6 +15,8 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import com.dougkeen.bart.model.Constants;
public class BartContentProvider extends ContentProvider { public class BartContentProvider extends ContentProvider {
private static final UriMatcher sUriMatcher; private static final UriMatcher sUriMatcher;
@ -95,14 +95,27 @@ public class BartContentProvider extends ContentProvider {
int match = sUriMatcher.match(uri); int match = sUriMatcher.match(uri);
if (match == ARBITRARY_ROUTE) { 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); MatrixCursor returnCursor = new MatrixCursor(projection);
RowBuilder newRow = returnCursor.newRow(); RowBuilder newRow = returnCursor.newRow();
for (String column : projection) { for (String column : projection) {
if (column.equals(RoutesColumns.FROM_STATION.string)) { if (column.equals(RoutesColumns.FROM_STATION.string)) {
newRow.add(uri.getPathSegments().get(1)); newRow.add(origin);
} else if (column.equals(RoutesColumns.TO_STATION.string)) { } else if (column.equals(RoutesColumns.TO_STATION.string)) {
newRow.add(uri.getPathSegments().get(2)); newRow.add(destination);
} else { } else {
newRow.add(null); newRow.add(null);
} }
@ -204,6 +217,24 @@ public class BartContentProvider extends ContentProvider {
+ ')' : ""), whereArgs); + ')' : ""), whereArgs);
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return count; 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; return 0;
} }

View File

@ -302,11 +302,17 @@ public class Departure implements Parcelable, Comparable<Departure> {
setMinEstimate(newMin); setMinEstimate(newMin);
setMaxEstimate(newMax); setMaxEstimate(newMax);
} }
if (!hasAnyArrivalEstimate() && departure.hasAnyArrivalEstimate()) {
setArrivalTimeOverride(departure.getArrivalTimeOverride());
setEstimatedTripTime(departure.getEstimatedTripTime());
}
} }
public int compareTo(Departure another) { public int compareTo(Departure another) {
return (this.getMinutes() > another.getMinutes()) ? 1 : ((this return (this.getMeanSecondsLeft() > another.getMeanSecondsLeft()) ? 1
.getMinutes() == another.getMinutes()) ? 0 : -1); : ((this.getMeanSecondsLeft() == another.getMeanSecondsLeft()) ? 0
: -1);
} }
@Override @Override

View File

@ -117,7 +117,8 @@ public class Route {
} else if (routeLine.transferLine2 != null } else if (routeLine.transferLine2 != null
&& viaLine.equals(routeLine.transferLine2)) { && viaLine.equals(routeLine.transferLine2)) {
return true; return true;
} else if (requiresTransfer && transferLines != null && !transferLines.isEmpty()) { } else if (requiresTransfer && transferLines != null
&& !transferLines.isEmpty()) {
return transferLines.contains(viaLine); return transferLines.contains(viaLine);
} else { } else {
int originIndex = viaLine.stations.indexOf(origin); int originIndex = viaLine.stations.indexOf(origin);

View File

@ -1,6 +1,5 @@
package com.dougkeen.bart.model; package com.dougkeen.bart.model;
public class StationPair { public class StationPair {
public StationPair(Station origin, Station destination) { public StationPair(Station origin, Station destination) {
super(); super();
@ -18,4 +17,36 @@ public class StationPair {
public Station getDestination() { public Station getDestination() {
return destination; 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;
}
} }

View File

@ -1,7 +1,6 @@
package com.dougkeen.bart.networktasks; package com.dougkeen.bart.networktasks;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;