From 7a4e8da4f0c5481776c849cabcb296987ad0e7f1 Mon Sep 17 00:00:00 2001 From: Doug Keen Date: Tue, 11 Sep 2012 15:49:36 -0700 Subject: [PATCH] Now uses service to grab departure estimates (in preparation for a notification service that will share that EtdService) --- .gitignore | 3 + AndroidManifest.xml | 9 +- .../simple_spinner_dropdown_item.xml | 14 - .../dougkeen/bart/AddRouteDialogFragment.java | 3 +- .../dougkeen/bart/DepartureArrayAdapter.java | 9 +- src/com/dougkeen/bart/EtdService.java | 580 ++++++++++++++++ src/com/dougkeen/bart/RoutesListActivity.java | 5 - .../dougkeen/bart/ViewDeparturesActivity.java | 638 ++++++------------ src/com/dougkeen/bart/ViewMapActivity.java | 2 - .../bart/controls/TimedTextSwitcher.java | 8 +- .../bart/data/BartContentProvider.java | 39 +- src/com/dougkeen/bart/model/Departure.java | 10 +- src/com/dougkeen/bart/model/Route.java | 3 +- src/com/dougkeen/bart/model/StationPair.java | 33 +- .../bart/networktasks/EtdContentHandler.java | 1 - 15 files changed, 878 insertions(+), 479 deletions(-) create mode 100644 .gitignore delete mode 100644 res/layout-v11/simple_spinner_dropdown_item.xml create mode 100644 src/com/dougkeen/bart/EtdService.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e80a6d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +/bin +/gen \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e2c4b81..d71fdbe 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,14 +2,14 @@ + android:versionCode="21" + android:versionName="2.1.0" > + + + \ No newline at end of file diff --git a/res/layout-v11/simple_spinner_dropdown_item.xml b/res/layout-v11/simple_spinner_dropdown_item.xml deleted file mode 100644 index 3493305..0000000 --- a/res/layout-v11/simple_spinner_dropdown_item.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/com/dougkeen/bart/AddRouteDialogFragment.java b/src/com/dougkeen/bart/AddRouteDialogFragment.java index c4e92d1..97279f5 100644 --- a/src/com/dougkeen/bart/AddRouteDialogFragment.java +++ b/src/com/dougkeen/bart/AddRouteDialogFragment.java @@ -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)) diff --git a/src/com/dougkeen/bart/DepartureArrayAdapter.java b/src/com/dougkeen/bart/DepartureArrayAdapter.java index 3345d2d..7e24629 100644 --- a/src/com/dougkeen/bart/DepartureArrayAdapter.java +++ b/src/com/dougkeen/bart/DepartureArrayAdapter.java @@ -72,7 +72,14 @@ public class DepartureArrayAdapter extends ArrayAdapter { .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) { diff --git a/src/com/dougkeen/bart/EtdService.java b/src/com/dougkeen/bart/EtdService.java new file mode 100644 index 0000000..f9572d3 --- /dev/null +++ b/src/com/dougkeen/bart/EtdService.java @@ -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 mServiceEngineMap; + + public EtdService() { + super(); + mBinder = new EtdServiceBinder(); + mServiceEngineMap = new HashMap(); + } + + 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 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 mListeners; + + private List mLatestDepartures; + private ScheduleInformation mLatestScheduleInfo; + + private AsyncTask mGetDeparturesTask; + private AsyncTask mGetScheduleInformationTask; + + private Handler mRunnableQueue; + + private boolean mStarted = false; + + public EtdServiceEngine(final StationPair route) { + mStationPair = route; + mListeners = new WeakHashMap(); + mRunnableQueue = new Handler(); + mLatestDepartures = new ArrayList(); + + 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 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"); + } + + } +} diff --git a/src/com/dougkeen/bart/RoutesListActivity.java b/src/com/dougkeen/bart/RoutesListActivity.java index 63cb67e..f1cfd55 100644 --- a/src/com/dougkeen/bart/RoutesListActivity.java +++ b/src/com/dougkeen/bart/RoutesListActivity.java @@ -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; diff --git a/src/com/dougkeen/bart/ViewDeparturesActivity.java b/src/com/dougkeen/bart/ViewDeparturesActivity.java index daf93f6..62b69b6 100644 --- a/src/com/dougkeen/bart/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/ViewDeparturesActivity.java @@ -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 mGetDeparturesTask; - private AsyncTask 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() { + @Override + public Loader 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 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 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) 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 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 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); + } } diff --git a/src/com/dougkeen/bart/ViewMapActivity.java b/src/com/dougkeen/bart/ViewMapActivity.java index c95dd18..ac9abd7 100644 --- a/src/com/dougkeen/bart/ViewMapActivity.java +++ b/src/com/dougkeen/bart/ViewMapActivity.java @@ -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 { diff --git a/src/com/dougkeen/bart/controls/TimedTextSwitcher.java b/src/com/dougkeen/bart/controls/TimedTextSwitcher.java index 5a6420f..209c3ce 100644 --- a/src/com/dougkeen/bart/controls/TimedTextSwitcher.java +++ b/src/com/dougkeen/bart/controls/TimedTextSwitcher.java @@ -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) { diff --git a/src/com/dougkeen/bart/data/BartContentProvider.java b/src/com/dougkeen/bart/data/BartContentProvider.java index f929fc9..bbae2dc 100644 --- a/src/com/dougkeen/bart/data/BartContentProvider.java +++ b/src/com/dougkeen/bart/data/BartContentProvider.java @@ -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; } diff --git a/src/com/dougkeen/bart/model/Departure.java b/src/com/dougkeen/bart/model/Departure.java index f9a5637..f26a696 100644 --- a/src/com/dougkeen/bart/model/Departure.java +++ b/src/com/dougkeen/bart/model/Departure.java @@ -302,11 +302,17 @@ public class Departure implements Parcelable, Comparable { 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 diff --git a/src/com/dougkeen/bart/model/Route.java b/src/com/dougkeen/bart/model/Route.java index e9ec275..6cd7f53 100644 --- a/src/com/dougkeen/bart/model/Route.java +++ b/src/com/dougkeen/bart/model/Route.java @@ -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); diff --git a/src/com/dougkeen/bart/model/StationPair.java b/src/com/dougkeen/bart/model/StationPair.java index 23f7a3e..31ed9f0 100644 --- a/src/com/dougkeen/bart/model/StationPair.java +++ b/src/com/dougkeen/bart/model/StationPair.java @@ -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; + } + } \ No newline at end of file diff --git a/src/com/dougkeen/bart/networktasks/EtdContentHandler.java b/src/com/dougkeen/bart/networktasks/EtdContentHandler.java index d310ae0..0ca3054 100644 --- a/src/com/dougkeen/bart/networktasks/EtdContentHandler.java +++ b/src/com/dougkeen/bart/networktasks/EtdContentHandler.java @@ -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;