diff --git a/res/layout/departure_listing.xml b/res/layout/departure_listing.xml index 9bbf0e4..54f24c5 100644 --- a/res/layout/departure_listing.xml +++ b/res/layout/departure_listing.xml @@ -52,7 +52,7 @@ android:layout_height="wrap_content" android:layout_below="@id/topRow" android:layout_toRightOf="@id/destinationColorBar" - bart:tickInterval="3" /> + bart:tickInterval="1" /> + + + + + + + + \ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml index d99f709..a537c8e 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -2,7 +2,13 @@ - + + - + - + - + - + \ No newline at end of file diff --git a/src/com/dougkeen/bart/DepartureArrayAdapter.java b/src/com/dougkeen/bart/DepartureArrayAdapter.java index de2be7b..3345d2d 100644 --- a/src/com/dougkeen/bart/DepartureArrayAdapter.java +++ b/src/com/dougkeen/bart/DepartureArrayAdapter.java @@ -73,23 +73,22 @@ public class DepartureArrayAdapter extends ArrayAdapter { initTextSwitcher(textSwitcher); textSwitcher.setCurrentText(departure.getTrainLengthText()); - textSwitcher.setTextProviders(new TextProvider[] { new TextProvider() { + textSwitcher.setTextProvider(new TextProvider() { @Override - public String getText() { - final String estimatedArrivalTimeText = departure - .getEstimatedArrivalTimeText(getContext()); - if (StringUtils.isBlank(estimatedArrivalTimeText)) { - return ""; + public String getText(long tickNumber) { + if (tickNumber % 4 == 0) { + return departure.getTrainLengthText(); } else { - return "Est. arrival " + estimatedArrivalTimeText; + final String estimatedArrivalTimeText = departure + .getEstimatedArrivalTimeText(getContext()); + if (StringUtils.isBlank(estimatedArrivalTimeText)) { + return ""; + } else { + return "Est. arrival " + estimatedArrivalTimeText; + } } } - }, new TextProvider() { - @Override - public String getText() { - return departure.getTrainLengthText(); - } - } }); + }); ImageView colorBar = (ImageView) view .findViewById(R.id.destinationColorBar); @@ -100,7 +99,7 @@ public class DepartureArrayAdapter extends ArrayAdapter { countdownTextView.setText(departure.getCountdownText()); countdownTextView.setTextProvider(new TextProvider() { @Override - public String getText() { + public String getText(long tickNumber) { return departure.getCountdownText(); } }); diff --git a/src/com/dougkeen/bart/TrainAlertDialogFragment.java b/src/com/dougkeen/bart/TrainAlertDialogFragment.java new file mode 100644 index 0000000..9386d46 --- /dev/null +++ b/src/com/dougkeen/bart/TrainAlertDialogFragment.java @@ -0,0 +1,89 @@ +package com.dougkeen.bart; + +import net.simonvt.widget.NumberPicker; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; + +import com.WazaBe.HoloEverywhere.AlertDialog; +import com.dougkeen.bart.model.Departure; + +public class TrainAlertDialogFragment extends DialogFragment { + + private static final String KEY_LAST_ALERT_DELAY = "lastAlertDelay"; + private Departure mDeparture; + + public TrainAlertDialogFragment(Departure mDeparture) { + super(); + this.mDeparture = mDeparture; + } + + @Override + public void onStart() { + super.onStart(); + + SharedPreferences preferences = getActivity().getPreferences( + Context.MODE_PRIVATE); + int lastAlertDelay = preferences.getInt(KEY_LAST_ALERT_DELAY, 5); + + NumberPicker numberPicker = (NumberPicker) getDialog().findViewById( + R.id.numberPicker); + + final int maxValue = mDeparture.getMeanSecondsLeft() / 60; + + String[] displayedValues = new String[maxValue]; + for (int i = 1; i <= maxValue; i++) { + displayedValues[i - 1] = String.valueOf(i); + } + numberPicker.setMinValue(1); + numberPicker.setMaxValue(maxValue); + numberPicker.setDisplayedValues(displayedValues); + + if (maxValue >= lastAlertDelay) { + numberPicker.setValue(lastAlertDelay); + } else if (maxValue >= 5) { + numberPicker.setValue(5); + } else if (maxValue >= 3) { + numberPicker.setValue(3); + } else { + numberPicker.setValue(1); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + + return new AlertDialog.Builder(activity) + .setTitle(R.string.set_up_departure_alert) + .setCancelable(true) + .setView(R.layout.train_alert_dialog) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + NumberPicker numberPicker = (NumberPicker) getDialog() + .findViewById(R.id.numberPicker); + + Editor editor = getActivity().getPreferences( + Context.MODE_PRIVATE).edit(); + editor.putInt(KEY_LAST_ALERT_DELAY, + numberPicker.getValue()); + editor.commit(); + } + }) + .setNegativeButton(R.string.skip_alert, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + dialog.cancel(); + } + }).create(); + } +} diff --git a/src/com/dougkeen/bart/ViewDeparturesActivity.java b/src/com/dougkeen/bart/ViewDeparturesActivity.java index af9c6bf..daf93f6 100644 --- a/src/com/dougkeen/bart/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/ViewDeparturesActivity.java @@ -19,13 +19,14 @@ import android.text.format.DateFormat; import android.text.util.Linkify; import android.util.Log; import android.view.View; +import android.widget.AdapterView; import android.widget.ImageView; -import android.widget.ListView; +import android.widget.ListAdapter; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockListActivity; +import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; @@ -44,12 +45,10 @@ import com.dougkeen.bart.model.TextProvider; import com.dougkeen.bart.networktasks.GetRealTimeDeparturesTask; import com.dougkeen.bart.networktasks.GetScheduleInformationTask; -public class ViewDeparturesActivity extends SherlockListActivity { +public class ViewDeparturesActivity extends SherlockFragmentActivity { private static final int UNCERTAINTY_THRESHOLD = 17; - private static final int DIALOG_SET_ALERT = 1; - private Uri mUri; private Station mOrigin; @@ -138,6 +137,20 @@ public class ViewDeparturesActivity extends SherlockListActivity { } } setListAdapter(mDeparturesAdapter); + getListView().setEmptyView(findViewById(android.R.id.empty)); + getListView().setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, + View view, int position, long id) { + mSelectedDeparture = (Departure) getListAdapter() + .getItem(position); + if (mActionMode != null) { + mActionMode.finish(); + } + startDepartureActionMode(); + } + }); findViewById(R.id.missingDepartureText).setVisibility(View.VISIBLE); @@ -147,6 +160,22 @@ public class ViewDeparturesActivity extends SherlockListActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } + @SuppressWarnings("unchecked") + private AdapterView getListView() { + return (AdapterView) findViewById(android.R.id.list); + } + + private DepartureArrayAdapter mListAdapter; + + protected DepartureArrayAdapter getListAdapter() { + return mListAdapter; + } + + protected void setListAdapter(DepartureArrayAdapter adapter) { + mListAdapter = adapter; + getListView().setAdapter(mListAdapter); + } + @Override protected void onPause() { cancelDataFetch(); @@ -310,9 +339,26 @@ public class ViewDeparturesActivity extends SherlockListActivity { } 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.getCount() > 0) { + 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++; @@ -321,37 +367,42 @@ public class ViewDeparturesActivity extends SherlockListActivity { 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; } } - } else { - for (Departure departure : departures) { - if (firstDeparture == null) { - firstDeparture = departure; - } - mDeparturesAdapter.add(departure); - } - needsBetterAccuracy = true; } mDeparturesAdapter.notifyDataSetChanged(); requestScheduleIfNecessary(); @@ -385,15 +436,19 @@ public class ViewDeparturesActivity extends SherlockListActivity { } 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 @@ -424,6 +479,7 @@ public class ViewDeparturesActivity extends SherlockListActivity { } } + // Match scheduled departures with real time departures in adapter int lastSearchIndex = 0; int tripCount = mLatestScheduleInfo.getTrips().size(); boolean departureUpdated = false; @@ -433,6 +489,7 @@ public class ViewDeparturesActivity extends SherlockListActivity { 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; @@ -478,17 +535,21 @@ public class ViewDeparturesActivity extends SherlockListActivity { 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++; } @@ -515,6 +576,7 @@ public class ViewDeparturesActivity extends SherlockListActivity { getContentResolver().update(mUri, contentValues, null, null); } + // If we still have some departures without estimates, try again later if (departuresWithoutEstimates > 0) { scheduleScheduleInfoFetch(20000); } @@ -618,7 +680,7 @@ public class ViewDeparturesActivity extends SherlockListActivity { + " " + departure.getUncertaintyText()); departureCountdown.setTextProvider(new TextProvider() { @Override - public String getText() { + public String getText(long tickNumber) { if (departure.hasDeparted()) { return "Departed"; } else { @@ -632,22 +694,13 @@ public class ViewDeparturesActivity extends SherlockListActivity { .getEstimatedArrivalMinutesLeftText(this)); arrivalCountdown.setTextProvider(new TextProvider() { @Override - public String getText() { + public String getText(long tickNumber) { return departure .getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this); } }); } - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - mSelectedDeparture = (Departure) getListAdapter().getItem(position); - if (mActionMode != null) { - mActionMode.finish(); - } - startDepartureActionMode(); - } - private void startDepartureActionMode() { mActionMode = startActionMode(new DepartureActionMode()); mActionMode.setTitle(mSelectedDeparture.getDestinationName()); @@ -672,6 +725,13 @@ public class ViewDeparturesActivity extends SherlockListActivity { if (item.getItemId() == R.id.boardTrain) { mBoardedDeparture = mSelectedDeparture; refreshBoardedDeparture(); + + // Don't prompt for alert if train is about to leave + if (mBoardedDeparture.getMeanSecondsLeft() / 60 > 1) { + new TrainAlertDialogFragment(mBoardedDeparture).show( + getSupportFragmentManager(), "dialog"); + } + mode.finish(); return true; } diff --git a/src/com/dougkeen/bart/controls/CountdownTextView.java b/src/com/dougkeen/bart/controls/CountdownTextView.java index c015d4e..447bfee 100644 --- a/src/com/dougkeen/bart/controls/CountdownTextView.java +++ b/src/com/dougkeen/bart/controls/CountdownTextView.java @@ -50,8 +50,8 @@ public class CountdownTextView extends TextView implements } @Override - public void onTick() { - setText(mTextProvider.getText()); + public void onTick(long tickNumber) { + setText(mTextProvider.getText(tickNumber)); } } diff --git a/src/com/dougkeen/bart/controls/Ticker.java b/src/com/dougkeen/bart/controls/Ticker.java index a9a9af9..8486fbd 100644 --- a/src/com/dougkeen/bart/controls/Ticker.java +++ b/src/com/dougkeen/bart/controls/Ticker.java @@ -4,13 +4,12 @@ import java.util.Iterator; import java.util.WeakHashMap; import android.os.Handler; -import android.util.Log; public class Ticker { public static interface TickSubscriber { int getTickInterval(); - void onTick(); + void onTick(long mTickCount); } private static Ticker sInstance; @@ -54,7 +53,7 @@ public class Ticker { stillHasListeners = true; if (subscriber.getTickInterval() > 0 && mTickCount % subscriber.getTickInterval() == 0) - subscriber.onTick(); + subscriber.onTick(mTickCount); } long endTimeNanos = System.nanoTime(); diff --git a/src/com/dougkeen/bart/controls/TimedTextSwitcher.java b/src/com/dougkeen/bart/controls/TimedTextSwitcher.java index 86ab955..5a6420f 100644 --- a/src/com/dougkeen/bart/controls/TimedTextSwitcher.java +++ b/src/com/dougkeen/bart/controls/TimedTextSwitcher.java @@ -30,8 +30,7 @@ public class TimedTextSwitcher extends TextSwitcher implements } private int mTickInterval; - private TextProvider[] mTextProviderArray; - private int mCurrentIndex = 0; + private TextProvider mTextProvider; @Override public int getTickInterval() { @@ -42,22 +41,21 @@ public class TimedTextSwitcher extends TextSwitcher implements this.mTickInterval = tickInterval; } - public void setTextProviders(TextProvider[] textProviders) { - mTextProviderArray = textProviders; + public void setTextProvider(TextProvider textProvider) { + mTextProvider = textProvider; Ticker.getInstance().addSubscriber(this); } private String mLastText; @Override - public void onTick() { - String text = mTextProviderArray[mCurrentIndex].getText(); + public void onTick(long tickNumber) { + String text = mTextProvider.getText(tickNumber); if (StringUtils.isNotBlank(text) && !StringUtils.equalsIgnoreCase(text, mLastText)) { mLastText = text; setText(text); } - mCurrentIndex = (mCurrentIndex + 1) % mTextProviderArray.length; } } diff --git a/src/com/dougkeen/bart/model/Departure.java b/src/com/dougkeen/bart/model/Departure.java index 553a824..f9a5637 100644 --- a/src/com/dougkeen/bart/model/Departure.java +++ b/src/com/dougkeen/bart/model/Departure.java @@ -405,7 +405,7 @@ public class Departure implements Parcelable, Comparable { if (hasDeparted()) { return ""; } else { - return "(±" + getUncertaintySeconds() + "s)"; + return "(±" + getUncertaintySeconds() + "s)"; } } diff --git a/src/com/dougkeen/bart/model/TextProvider.java b/src/com/dougkeen/bart/model/TextProvider.java index 35dc3a3..fe42318 100644 --- a/src/com/dougkeen/bart/model/TextProvider.java +++ b/src/com/dougkeen/bart/model/TextProvider.java @@ -2,6 +2,6 @@ package com.dougkeen.bart.model; public interface TextProvider { - String getText(); + String getText(long tickNumber); }