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_below="@id/topRow"
android:layout_toRightOf="@id/destinationColorBar"
bart:tickInterval="3" />
bart:tickInterval="1" />
<TextView
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">
<!-- 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">
<item name="android:layout_width">fill_parent</item>

View File

@ -73,23 +73,22 @@ public class DepartureArrayAdapter extends ArrayAdapter<Departure> {
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<Departure> {
countdownTextView.setText(departure.getCountdownText());
countdownTextView.setTextProvider(new TextProvider() {
@Override
public String getText() {
public String getText(long tickNumber) {
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.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<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
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<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;
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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