Changed train length/estimated arrival change timing

Prototyped departure alert dialog
This commit is contained in:
Doug Keen 2012-09-06 22:47:27 -07:00
parent 4b94669aa7
commit 64d5eda3b5
11 changed files with 235 additions and 59 deletions

View File

@ -52,7 +52,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/topRow" android:layout_below="@id/topRow"
android:layout_toRightOf="@id/destinationColorBar" android:layout_toRightOf="@id/destinationColorBar"
bart:tickInterval="3" /> bart:tickInterval="1" />
<TextView <TextView
android:id="@+id/uncertainty" android:id="@+id/uncertainty"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<net.simonvt.widget.NumberPicker
android:id="@+id/numberPicker"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="1" >
</net.simonvt.widget.NumberPicker>
<TextView
android:id="@+id/textView1"
style="@style/LargeText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:maxLines="3"
android:singleLine="false"
android:text="minutes before departure" />
</LinearLayout>

View File

@ -2,7 +2,13 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Base application theme is the default theme. --> <!-- Base application theme is the default theme. -->
<style name="AppTheme" parent="@style/Theme.HoloEverywhereDark.Sherlock"></style>
<style name="AppTheme" parent="@style/Theme.HoloEverywhereDark.Sherlock">
<item name="numberPickerUpButtonStyle">@style/NPWidget.Holo.ImageButton.NumberPickerUpButton</item>
<item name="numberPickerDownButtonStyle">@style/NPWidget.Holo.ImageButton.NumberPickerDownButton</item>
<item name="numberPickerInputTextStyle">@style/NPWidget.Holo.EditText.NumberPickerInputText</item>
<item name="numberPickerStyle">@style/NPWidget.Holo.NumberPicker</item>
</style>
<style name="ButtonBar"> <style name="ButtonBar">
<item name="android:layout_width">fill_parent</item> <item name="android:layout_width">fill_parent</item>

View File

@ -73,23 +73,22 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
initTextSwitcher(textSwitcher); initTextSwitcher(textSwitcher);
textSwitcher.setCurrentText(departure.getTrainLengthText()); textSwitcher.setCurrentText(departure.getTrainLengthText());
textSwitcher.setTextProviders(new TextProvider[] { new TextProvider() { textSwitcher.setTextProvider(new TextProvider() {
@Override @Override
public String getText() { public String getText(long tickNumber) {
final String estimatedArrivalTimeText = departure if (tickNumber % 4 == 0) {
.getEstimatedArrivalTimeText(getContext()); return departure.getTrainLengthText();
if (StringUtils.isBlank(estimatedArrivalTimeText)) {
return "";
} else { } 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 ImageView colorBar = (ImageView) view
.findViewById(R.id.destinationColorBar); .findViewById(R.id.destinationColorBar);
@ -100,7 +99,7 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
countdownTextView.setText(departure.getCountdownText()); countdownTextView.setText(departure.getCountdownText());
countdownTextView.setTextProvider(new TextProvider() { countdownTextView.setTextProvider(new TextProvider() {
@Override @Override
public String getText() { public String getText(long tickNumber) {
return departure.getCountdownText(); return departure.getCountdownText();
} }
}); });

View File

@ -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();
}
}

View File

@ -19,13 +19,14 @@ import android.text.format.DateFormat;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ListView; import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.actionbarsherlock.app.SherlockListActivity; 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;
import com.actionbarsherlock.view.MenuInflater; 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.GetRealTimeDeparturesTask;
import com.dougkeen.bart.networktasks.GetScheduleInformationTask; 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 UNCERTAINTY_THRESHOLD = 17;
private static final int DIALOG_SET_ALERT = 1;
private Uri mUri; private Uri mUri;
private Station mOrigin; private Station mOrigin;
@ -138,6 +137,20 @@ public class ViewDeparturesActivity extends SherlockListActivity {
} }
} }
setListAdapter(mDeparturesAdapter); 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); findViewById(R.id.missingDepartureText).setVisibility(View.VISIBLE);
@ -147,6 +160,22 @@ public class ViewDeparturesActivity extends SherlockListActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} }
@SuppressWarnings("unchecked")
private AdapterView<ListAdapter> getListView() {
return (AdapterView<ListAdapter>) findViewById(android.R.id.list);
}
private DepartureArrayAdapter mListAdapter;
protected DepartureArrayAdapter getListAdapter() {
return mListAdapter;
}
protected void setListAdapter(DepartureArrayAdapter adapter) {
mListAdapter = adapter;
getListView().setAdapter(mListAdapter);
}
@Override @Override
protected void onPause() { protected void onPause() {
cancelDataFetch(); cancelDataFetch();
@ -310,9 +339,26 @@ public class ViewDeparturesActivity extends SherlockListActivity {
} }
boolean needsBetterAccuracy = false; boolean needsBetterAccuracy = false;
// Keep track of first departure, since we'll request another quick
// refresh if it has departed.
Departure firstDeparture = null; Departure firstDeparture = null;
final List<Departure> departures = result.getDepartures(); final List<Departure> 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; int adapterIndex = -1;
for (Departure departure : departures) { for (Departure departure : departures) {
adapterIndex++; adapterIndex++;
@ -321,37 +367,42 @@ public class ViewDeparturesActivity extends SherlockListActivity {
existingDeparture = mDeparturesAdapter existingDeparture = mDeparturesAdapter
.getItem(adapterIndex); .getItem(adapterIndex);
} }
// Looks for departures at the beginning of the adapter that
// aren't in the latest list of departures
while (existingDeparture != null while (existingDeparture != null
&& !departure.equals(existingDeparture)) { && !departure.equals(existingDeparture)) {
// Remove old departure
mDeparturesAdapter.remove(existingDeparture); mDeparturesAdapter.remove(existingDeparture);
if (adapterIndex < mDeparturesAdapter.getCount()) { 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 existingDeparture = mDeparturesAdapter
.getItem(adapterIndex); .getItem(adapterIndex);
} else { } else {
// Reached the end of the adapter... give up
existingDeparture = null; existingDeparture = null;
} }
} }
// Merge the estimate if we found a matching departure,
// otherwise add a new one to the adapter
if (existingDeparture != null) { if (existingDeparture != null) {
existingDeparture.mergeEstimate(departure); existingDeparture.mergeEstimate(departure);
} else { } else {
mDeparturesAdapter.add(departure); mDeparturesAdapter.add(departure);
existingDeparture = departure; existingDeparture = departure;
} }
// Set first departure
if (firstDeparture == null) { if (firstDeparture == null) {
firstDeparture = existingDeparture; firstDeparture = existingDeparture;
} }
// Check if estimate is accurate enough
if (existingDeparture.getUncertaintySeconds() > UNCERTAINTY_THRESHOLD) { if (existingDeparture.getUncertaintySeconds() > UNCERTAINTY_THRESHOLD) {
needsBetterAccuracy = true; needsBetterAccuracy = true;
} }
} }
} else {
for (Departure departure : departures) {
if (firstDeparture == null) {
firstDeparture = departure;
}
mDeparturesAdapter.add(departure);
}
needsBetterAccuracy = true;
} }
mDeparturesAdapter.notifyDataSetChanged(); mDeparturesAdapter.notifyDataSetChanged();
requestScheduleIfNecessary(); requestScheduleIfNecessary();
@ -385,15 +436,19 @@ public class ViewDeparturesActivity extends SherlockListActivity {
} }
private void requestScheduleIfNecessary() { private void requestScheduleIfNecessary() {
// Bail if there's nothing to match schedules to
if (mDeparturesAdapter.getCount() == 0) { if (mDeparturesAdapter.getCount() == 0) {
return; return;
} }
// Fetch if we don't have anything at all
if (mLatestScheduleInfo == null) { if (mLatestScheduleInfo == null) {
fetchLatestSchedule(); fetchLatestSchedule();
return; return;
} }
// Otherwise, check if the latest departure doesn't have schedule
// info... if not, fetch
Departure lastDeparture = mDeparturesAdapter.getItem(mDeparturesAdapter Departure lastDeparture = mDeparturesAdapter.getItem(mDeparturesAdapter
.getCount() - 1); .getCount() - 1);
if (mLatestScheduleInfo.getLatestDepartureTime() < lastDeparture 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 lastSearchIndex = 0;
int tripCount = mLatestScheduleInfo.getTrips().size(); int tripCount = mLatestScheduleInfo.getTrips().size();
boolean departureUpdated = false; boolean departureUpdated = false;
@ -433,6 +489,7 @@ public class ViewDeparturesActivity extends SherlockListActivity {
Departure departure = mDeparturesAdapter.getItem(departureIndex); Departure departure = mDeparturesAdapter.getItem(departureIndex);
for (int i = lastSearchIndex; i < tripCount; i++) { for (int i = lastSearchIndex; i < tripCount; i++) {
ScheduleItem trip = mLatestScheduleInfo.getTrips().get(i); ScheduleItem trip = mLatestScheduleInfo.getTrips().get(i);
// Definitely not a match if they have different destinations
if (!departure.getDestination().abbreviation.equals(trip if (!departure.getDestination().abbreviation.equals(trip
.getTrainHeadStation())) { .getTrainHeadStation())) {
continue; continue;
@ -478,17 +535,21 @@ public class ViewDeparturesActivity extends SherlockListActivity {
break; break;
} }
} }
// Don't estimate for non-scheduled transfers // Don't estimate for non-scheduled transfers
if (!departure.getRequiresTransfer()) { if (!departure.getRequiresTransfer()) {
if (!departure.hasEstimatedTripTime() && localAverageLength > 0) { if (!departure.hasEstimatedTripTime() && localAverageLength > 0) {
// Use the average we just calculated if available
departure.setEstimatedTripTime(localAverageLength); departure.setEstimatedTripTime(localAverageLength);
} else if (!departure.hasEstimatedTripTime()) { } else if (!departure.hasEstimatedTripTime()) {
// Otherwise just assume the global average
departure.setEstimatedTripTime(mAverageTripLength); departure.setEstimatedTripTime(mAverageTripLength);
} }
} else if (departure.getRequiresTransfer() } else if (departure.getRequiresTransfer()
&& !departure.hasAnyArrivalEstimate()) { && !departure.hasAnyArrivalEstimate()) {
lastUnestimatedTransfer = departure; lastUnestimatedTransfer = departure;
} }
if (!departure.hasAnyArrivalEstimate()) { if (!departure.hasAnyArrivalEstimate()) {
departuresWithoutEstimates++; departuresWithoutEstimates++;
} }
@ -515,6 +576,7 @@ public class ViewDeparturesActivity extends SherlockListActivity {
getContentResolver().update(mUri, contentValues, null, null); getContentResolver().update(mUri, contentValues, null, null);
} }
// If we still have some departures without estimates, try again later
if (departuresWithoutEstimates > 0) { if (departuresWithoutEstimates > 0) {
scheduleScheduleInfoFetch(20000); scheduleScheduleInfoFetch(20000);
} }
@ -618,7 +680,7 @@ public class ViewDeparturesActivity extends SherlockListActivity {
+ " " + departure.getUncertaintyText()); + " " + departure.getUncertaintyText());
departureCountdown.setTextProvider(new TextProvider() { departureCountdown.setTextProvider(new TextProvider() {
@Override @Override
public String getText() { public String getText(long tickNumber) {
if (departure.hasDeparted()) { if (departure.hasDeparted()) {
return "Departed"; return "Departed";
} else { } else {
@ -632,22 +694,13 @@ public class ViewDeparturesActivity extends SherlockListActivity {
.getEstimatedArrivalMinutesLeftText(this)); .getEstimatedArrivalMinutesLeftText(this));
arrivalCountdown.setTextProvider(new TextProvider() { arrivalCountdown.setTextProvider(new TextProvider() {
@Override @Override
public String getText() { public String getText(long tickNumber) {
return departure return departure
.getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this); .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() { private void startDepartureActionMode() {
mActionMode = startActionMode(new DepartureActionMode()); mActionMode = startActionMode(new DepartureActionMode());
mActionMode.setTitle(mSelectedDeparture.getDestinationName()); mActionMode.setTitle(mSelectedDeparture.getDestinationName());
@ -672,6 +725,13 @@ public class ViewDeparturesActivity extends SherlockListActivity {
if (item.getItemId() == R.id.boardTrain) { if (item.getItemId() == R.id.boardTrain) {
mBoardedDeparture = mSelectedDeparture; mBoardedDeparture = mSelectedDeparture;
refreshBoardedDeparture(); 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(); mode.finish();
return true; return true;
} }

View File

@ -50,8 +50,8 @@ public class CountdownTextView extends TextView implements
} }
@Override @Override
public void onTick() { public void onTick(long tickNumber) {
setText(mTextProvider.getText()); setText(mTextProvider.getText(tickNumber));
} }
} }

View File

@ -4,13 +4,12 @@ import java.util.Iterator;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import android.os.Handler; import android.os.Handler;
import android.util.Log;
public class Ticker { public class Ticker {
public static interface TickSubscriber { public static interface TickSubscriber {
int getTickInterval(); int getTickInterval();
void onTick(); void onTick(long mTickCount);
} }
private static Ticker sInstance; private static Ticker sInstance;
@ -54,7 +53,7 @@ public class Ticker {
stillHasListeners = true; stillHasListeners = true;
if (subscriber.getTickInterval() > 0 if (subscriber.getTickInterval() > 0
&& mTickCount % subscriber.getTickInterval() == 0) && mTickCount % subscriber.getTickInterval() == 0)
subscriber.onTick(); subscriber.onTick(mTickCount);
} }
long endTimeNanos = System.nanoTime(); long endTimeNanos = System.nanoTime();

View File

@ -30,8 +30,7 @@ public class TimedTextSwitcher extends TextSwitcher implements
} }
private int mTickInterval; private int mTickInterval;
private TextProvider[] mTextProviderArray; private TextProvider mTextProvider;
private int mCurrentIndex = 0;
@Override @Override
public int getTickInterval() { public int getTickInterval() {
@ -42,22 +41,21 @@ public class TimedTextSwitcher extends TextSwitcher implements
this.mTickInterval = tickInterval; this.mTickInterval = tickInterval;
} }
public void setTextProviders(TextProvider[] textProviders) { public void setTextProvider(TextProvider textProvider) {
mTextProviderArray = textProviders; mTextProvider = textProvider;
Ticker.getInstance().addSubscriber(this); Ticker.getInstance().addSubscriber(this);
} }
private String mLastText; private String mLastText;
@Override @Override
public void onTick() { public void onTick(long tickNumber) {
String text = mTextProviderArray[mCurrentIndex].getText(); String text = mTextProvider.getText(tickNumber);
if (StringUtils.isNotBlank(text) if (StringUtils.isNotBlank(text)
&& !StringUtils.equalsIgnoreCase(text, mLastText)) { && !StringUtils.equalsIgnoreCase(text, mLastText)) {
mLastText = text; mLastText = text;
setText(text); setText(text);
} }
mCurrentIndex = (mCurrentIndex + 1) % mTextProviderArray.length;
} }
} }

View File

@ -405,7 +405,7 @@ public class Departure implements Parcelable, Comparable<Departure> {
if (hasDeparted()) { if (hasDeparted()) {
return ""; return "";
} else { } else {
return "" + getUncertaintySeconds() + "s)"; return "" + getUncertaintySeconds() + "s)";
} }
} }

View File

@ -2,6 +2,6 @@ package com.dougkeen.bart.model;
public interface TextProvider { public interface TextProvider {
String getText(); String getText(long tickNumber);
} }