Refactored boarded departure alarms/notifications

Departure single click now selects boarded departure
"Your train" now has its own context menu/action bar
This commit is contained in:
Doug Keen 2012-10-01 21:03:08 -07:00
parent 3d3a30a563
commit 82f637b041
30 changed files with 659 additions and 311 deletions

View File

@ -67,7 +67,7 @@
android:label="BartRunner data provider" /> android:label="BartRunner data provider" />
<service <service
android:name=".services.NotificationService" android:name=".services.BoardedDepartureService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".services.EtdService" android:name=".services.EtdService"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -14,81 +14,14 @@
android:paddingRight="5dp" android:paddingRight="5dp"
android:textSize="24dp" /> android:textSize="24dp" />
<RelativeLayout <com.dougkeen.bart.controls.YourTrainLayout
android:id="@+id/yourTrainSection" android:id="@+id/yourTrainSection"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:background="#222"
android:padding="10dp" android:padding="10dp"
android:visibility="gone" > android:visibility="gone" >
</com.dougkeen.bart.controls.YourTrainLayout>
<TextView
android:id="@+id/yourTrainHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/your_train"
android:textAllCaps="true"
android:textSize="20dp"
android:textStyle="bold" />
<ImageView
android:id="@+id/yourTrainDestinationColorBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@id/yourTrainHeader"
android:src="@drawable/basic_rectangle" />
<TextView
android:id="@+id/yourTrainDestinationText"
style="@style/DepartureDestinationText"
android:layout_below="@id/yourTrainHeader"
android:layout_toRightOf="@id/yourTrainDestinationColorBar"
android:ellipsize="marquee"
android:singleLine="true" />
<ImageView
android:id="@+id/yourTrainBikeIcon"
style="@style/BikeIcon"
android:layout_below="@id/yourTrainHeader"
android:layout_toRightOf="@id/yourTrainDestinationText"
android:src="@drawable/bike" />
<ImageView
android:id="@+id/yourTrainXferIcon"
style="@style/XferIcon"
android:layout_below="@id/yourTrainBikeIcon"
android:layout_toRightOf="@id/yourTrainDestinationText"
android:src="@drawable/xfer" />
<TextView
android:id="@+id/yourTrainTrainLengthText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/yourTrainDestinationText"
android:layout_toRightOf="@id/yourTrainDestinationColorBar"
android:paddingLeft="5dp" />
<com.dougkeen.bart.controls.CountdownTextView
android:id="@+id/yourTrainDepartureCountdown"
style="@style/DepartureCountdownText"
android:layout_alignLeft="@id/yourTrainSection"
android:layout_alignRight="@id/yourTrainSection"
android:layout_below="@id/yourTrainTrainLengthText"
bart:tickInterval="1" />
<com.dougkeen.bart.controls.CountdownTextView
android:id="@+id/yourTrainArrivalCountdown"
style="@style/DepartureCountdownText"
android:layout_alignLeft="@id/yourTrainSection"
android:layout_alignRight="@id/yourTrainSection"
android:layout_below="@id/yourTrainDepartureCountdown"
android:ellipsize="end"
bart:tickInterval="5" />
</RelativeLayout>
<ProgressBar <ProgressBar
android:id="@android:id/progress" android:id="@android:id/progress"

71
res/layout/your_train.xml Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bart="http://schemas.android.com/apk/res/com.dougkeen.bart" >
<TextView
android:id="@+id/yourTrainHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/your_train"
android:textAllCaps="true"
android:textSize="20dp"
android:textStyle="bold" />
<ImageView
android:id="@+id/yourTrainDestinationColorBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@id/yourTrainHeader"
android:src="@drawable/basic_rectangle" />
<TextView
android:id="@+id/yourTrainDestinationText"
style="@style/DepartureDestinationText"
android:layout_below="@id/yourTrainHeader"
android:layout_toRightOf="@id/yourTrainDestinationColorBar"
android:ellipsize="marquee"
android:singleLine="true" />
<ImageView
android:id="@+id/yourTrainBikeIcon"
style="@style/BikeIcon"
android:layout_below="@id/yourTrainHeader"
android:layout_toRightOf="@id/yourTrainDestinationText"
android:src="@drawable/bike" />
<ImageView
android:id="@+id/yourTrainXferIcon"
style="@style/XferIcon"
android:layout_below="@id/yourTrainBikeIcon"
android:layout_toRightOf="@id/yourTrainDestinationText"
android:src="@drawable/xfer" />
<TextView
android:id="@+id/yourTrainTrainLengthText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/yourTrainDestinationText"
android:layout_toRightOf="@id/yourTrainDestinationColorBar"
android:paddingLeft="5dp" />
<com.dougkeen.bart.controls.CountdownTextView
android:id="@+id/yourTrainDepartureCountdown"
style="@style/DepartureCountdownText"
android:layout_alignLeft="@id/yourTrainSection"
android:layout_alignRight="@id/yourTrainSection"
android:layout_below="@id/yourTrainTrainLengthText"
bart:tickInterval="1" />
<com.dougkeen.bart.controls.CountdownTextView
android:id="@+id/yourTrainArrivalCountdown"
style="@style/DepartureCountdownText"
android:layout_alignLeft="@id/yourTrainSection"
android:layout_alignRight="@id/yourTrainSection"
android:layout_below="@id/yourTrainDepartureCountdown"
android:ellipsize="end"
bart:tickInterval="5" />
</merge>

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/cancel_alarm_button"
android:icon="@drawable/ic_action_cancel_alarm"
android:showAsAction="always|withText"
android:title="Cancel alarm"
android:visible="false"/>
<item <item
android:id="@+id/view_on_bart_site_button" android:id="@+id/view_on_bart_site_button"
android:icon="@drawable/ic_action_web" android:icon="@drawable/ic_action_web"

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/cancel_alarm_button"
android:icon="@drawable/ic_action_cancel_alarm"
android:showAsAction="always|withText"
android:title="@string/cancel_alarm"
android:visible="false"/>
<item
android:id="@+id/set_alarm_button"
android:icon="@drawable/ic_action_alarm"
android:showAsAction="always|withText"
android:title="@string/set_alarm"/>
<item
android:id="@+id/delete"
android:icon="@drawable/ic_action_delete"
android:showAsAction="always|withText"
android:title="@string/delete">
</item>
</menu>

View File

@ -3,5 +3,6 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="blue_selection">#FF2A7998</color> <color name="blue_selection">#FF2A7998</color>
<color name="gray">#FF222222</color>
</resources> </resources>

View File

@ -4,7 +4,7 @@
<string name="app_name">BART Runner</string> <string name="app_name">BART Runner</string>
<string name="favorite_routes">Favorite routes</string> <string name="favorite_routes">Favorite routes</string>
<string name="empty_favorites_list_message">No favorite routes have been added yet</string> <string name="empty_favorites_list_message">No favorite routes have been added yet</string>
<string name="loading_message">Loading, please wait...</string> <string name="loading_message">Loading, please wait&#8230;</string>
<string name="add_route">Add a route</string> <string name="add_route">Add a route</string>
<string name="origin">Origin</string> <string name="origin">Origin</string>
<string name="destination">Destination</string> <string name="destination">Destination</string>
@ -37,9 +37,10 @@
<string name="getting_on_this_train">I will board this train</string> <string name="getting_on_this_train">I will board this train</string>
<string name="departure_options">Departure options</string> <string name="departure_options">Departure options</string>
<string name="your_train">Your train</string> <string name="your_train">Your train</string>
<string name="skip_alert">Skip alert</string> <string name="set_up_departure_alarm">Set up departure alarm</string>
<string name="set_up_departure_alert">Set up departure alert</string> <string name="train_alarm_text">Your train is leaving soon!</string>
<string name="train_alert_text">Your train is leaving soon!</string>
<string name="silence_alarm">Silence alarm</string> <string name="silence_alarm">Silence alarm</string>
<string name="cancel_alarm">Cancel alarm</string>
<string name="set_alarm">Set alarm</string>
</resources> </resources>

View File

@ -17,7 +17,6 @@ import android.util.Log;
import com.dougkeen.bart.model.Constants; import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure; import com.dougkeen.bart.model.Departure;
import com.dougkeen.util.Observable;
public class BartRunnerApplication extends Application { public class BartRunnerApplication extends Application {
private static final int FIVE_MINUTES = 5 * 60 * 1000; private static final int FIVE_MINUTES = 5 * 60 * 1000;
@ -26,8 +25,6 @@ public class BartRunnerApplication extends Application {
private Departure mBoardedDeparture; private Departure mBoardedDeparture;
private Observable<Boolean> mAlarmPending = new Observable<Boolean>(false);
private boolean mPlayAlarmRingtone; private boolean mPlayAlarmRingtone;
private boolean mAlarmSounding; private boolean mAlarmSounding;
@ -84,24 +81,42 @@ public class BartRunnerApplication extends Application {
} }
} }
} }
if (mBoardedDeparture != null && mBoardedDeparture.hasExpired()) {
setBoardedDeparture(null);
}
return mBoardedDeparture; return mBoardedDeparture;
} }
public void setBoardedDeparture(Departure boardedDeparture) { public void setBoardedDeparture(Departure boardedDeparture) {
if (!ObjectUtils.equals(boardedDeparture, mBoardedDeparture) if (!ObjectUtils.equals(boardedDeparture, mBoardedDeparture)
|| ObjectUtils.compare(mBoardedDeparture, boardedDeparture) != 0) { || ObjectUtils.compare(mBoardedDeparture, boardedDeparture) != 0) {
// Cancel any pending alarms for the current departure if (this.mBoardedDeparture != null) {
if (this.mBoardedDeparture != null this.mBoardedDeparture.getAlarmLeadTimeMinutesObservable()
&& this.mBoardedDeparture.isAlarmPending()) { .unregisterAllObservers();
this.mBoardedDeparture.cancelAlarm(this, this.mBoardedDeparture.getAlarmPendingObservable()
(AlarmManager) getSystemService(Context.ALARM_SERVICE)); .unregisterAllObservers();
// Cancel any pending alarms for the current departure
if (this.mBoardedDeparture.isAlarmPending()) {
this.mBoardedDeparture
.cancelAlarm(
this,
(AlarmManager) getSystemService(Context.ALARM_SERVICE));
}
} }
this.mBoardedDeparture = boardedDeparture; this.mBoardedDeparture = boardedDeparture;
if (mBoardedDeparture != null) { File cachedDepartureFile = new File(getCacheDir(), CACHE_FILE_NAME);
File cachedDepartureFile = new File(getCacheDir(), if (mBoardedDeparture == null) {
CACHE_FILE_NAME); try {
cachedDepartureFile.delete();
} catch (SecurityException anotherException) {
Log.w(Constants.TAG,
"Couldn't delete lastBoardedDeparture file",
anotherException);
}
} else {
FileOutputStream fileOutputStream = null; FileOutputStream fileOutputStream = null;
try { try {
fileOutputStream = new FileOutputStream(cachedDepartureFile); fileOutputStream = new FileOutputStream(cachedDepartureFile);
@ -134,17 +149,4 @@ public class BartRunnerApplication extends Application {
public void setAlarmMediaPlayer(MediaPlayer alarmMediaPlayer) { public void setAlarmMediaPlayer(MediaPlayer alarmMediaPlayer) {
this.mAlarmMediaPlayer = alarmMediaPlayer; this.mAlarmMediaPlayer = alarmMediaPlayer;
} }
public boolean isAlarmPending() {
return mAlarmPending.getValue();
}
public Observable<Boolean> getAlarmPendingObservable() {
return mAlarmPending;
}
public void setAlarmPending(boolean alarmPending) {
this.mAlarmPending.setValue(alarmPending);
}
} }

View File

@ -13,12 +13,13 @@ import android.support.v4.app.FragmentActivity;
import com.WazaBe.HoloEverywhere.AlertDialog; import com.WazaBe.HoloEverywhere.AlertDialog;
import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.R; import com.dougkeen.bart.R;
import com.dougkeen.bart.model.Departure;
public class TrainAlertDialogFragment extends DialogFragment { public class TrainAlarmDialogFragment extends DialogFragment {
private static final String KEY_LAST_ALERT_LEAD_TIME = "lastAlertLeadTime"; private static final String KEY_LAST_ALARM_LEAD_TIME = "lastAlarmLeadTime";
public TrainAlertDialogFragment() { public TrainAlarmDialogFragment() {
super(); super();
} }
@ -28,7 +29,7 @@ public class TrainAlertDialogFragment extends DialogFragment {
SharedPreferences preferences = getActivity().getPreferences( SharedPreferences preferences = getActivity().getPreferences(
Context.MODE_PRIVATE); Context.MODE_PRIVATE);
int lastAlertLeadTime = preferences.getInt(KEY_LAST_ALERT_LEAD_TIME, 5); int lastAlarmLeadTime = preferences.getInt(KEY_LAST_ALARM_LEAD_TIME, 5);
NumberPicker numberPicker = (NumberPicker) getDialog().findViewById( NumberPicker numberPicker = (NumberPicker) getDialog().findViewById(
R.id.numberPicker); R.id.numberPicker);
@ -36,8 +37,8 @@ public class TrainAlertDialogFragment extends DialogFragment {
BartRunnerApplication application = (BartRunnerApplication) getActivity() BartRunnerApplication application = (BartRunnerApplication) getActivity()
.getApplication(); .getApplication();
final int maxValue = application.getBoardedDeparture() final Departure boardedDeparture = application.getBoardedDeparture();
.getMeanSecondsLeft() / 60; final int maxValue = boardedDeparture.getMeanSecondsLeft() / 60;
String[] displayedValues = new String[maxValue]; String[] displayedValues = new String[maxValue];
for (int i = 1; i <= maxValue; i++) { for (int i = 1; i <= maxValue; i++) {
@ -47,8 +48,10 @@ public class TrainAlertDialogFragment extends DialogFragment {
numberPicker.setMaxValue(maxValue); numberPicker.setMaxValue(maxValue);
numberPicker.setDisplayedValues(displayedValues); numberPicker.setDisplayedValues(displayedValues);
if (maxValue >= lastAlertLeadTime) { if (boardedDeparture.isAlarmPending()) {
numberPicker.setValue(lastAlertLeadTime); numberPicker.setValue(boardedDeparture.getAlarmLeadTimeMinutes());
} else if (maxValue >= lastAlarmLeadTime) {
numberPicker.setValue(lastAlarmLeadTime);
} else if (maxValue >= 5) { } else if (maxValue >= 5) {
numberPicker.setValue(5); numberPicker.setValue(5);
} else if (maxValue >= 3) { } else if (maxValue >= 3) {
@ -63,9 +66,9 @@ public class TrainAlertDialogFragment extends DialogFragment {
final FragmentActivity activity = getActivity(); final FragmentActivity activity = getActivity();
return new AlertDialog.Builder(activity) return new AlertDialog.Builder(activity)
.setTitle(R.string.set_up_departure_alert) .setTitle(R.string.set_up_departure_alarm)
.setCancelable(true) .setCancelable(true)
.setView(R.layout.train_alert_dialog) .setView(R.layout.train_alarm_dialog)
.setPositiveButton(R.string.ok, .setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
@Override @Override
@ -73,23 +76,23 @@ public class TrainAlertDialogFragment extends DialogFragment {
int which) { int which) {
NumberPicker numberPicker = (NumberPicker) getDialog() NumberPicker numberPicker = (NumberPicker) getDialog()
.findViewById(R.id.numberPicker); .findViewById(R.id.numberPicker);
final int alertLeadTime = numberPicker final int alarmLeadTime = numberPicker
.getValue(); .getValue();
// Save most recent selection // Save most recent selection
Editor editor = getActivity().getPreferences( Editor editor = getActivity().getPreferences(
Context.MODE_PRIVATE).edit(); Context.MODE_PRIVATE).edit();
editor.putInt(KEY_LAST_ALERT_LEAD_TIME, editor.putInt(KEY_LAST_ALARM_LEAD_TIME,
alertLeadTime); alarmLeadTime);
editor.commit(); editor.commit();
((BartRunnerApplication) getActivity() ((BartRunnerApplication) getActivity()
.getApplication()) .getApplication())
.getBoardedDeparture().setUpAlarm( .getBoardedDeparture().setUpAlarm(
alertLeadTime); alarmLeadTime);
} }
}) })
.setNegativeButton(R.string.skip_alert, .setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, public void onClick(DialogInterface dialog,
int whichButton) { int whichButton) {

View File

@ -10,8 +10,6 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
@ -28,7 +26,7 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ImageView; import android.widget.Checkable;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
@ -41,19 +39,18 @@ import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem;
import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.R; import com.dougkeen.bart.R;
import com.dougkeen.bart.controls.CountdownTextView;
import com.dougkeen.bart.controls.Ticker; import com.dougkeen.bart.controls.Ticker;
import com.dougkeen.bart.controls.YourTrainLayout;
import com.dougkeen.bart.data.DepartureArrayAdapter; import com.dougkeen.bart.data.DepartureArrayAdapter;
import com.dougkeen.bart.data.RoutesColumns; import com.dougkeen.bart.data.RoutesColumns;
import com.dougkeen.bart.model.Constants; import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure; import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.Station; import com.dougkeen.bart.model.Station;
import com.dougkeen.bart.model.StationPair; import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.model.TextProvider; import com.dougkeen.bart.services.BoardedDepartureService;
import com.dougkeen.bart.services.EtdService; import com.dougkeen.bart.services.EtdService;
import com.dougkeen.bart.services.EtdService.EtdServiceBinder; import com.dougkeen.bart.services.EtdService.EtdServiceBinder;
import com.dougkeen.bart.services.EtdService.EtdServiceListener; import com.dougkeen.bart.services.EtdService.EtdServiceListener;
import com.dougkeen.bart.services.NotificationService;
import com.dougkeen.util.Observer; import com.dougkeen.util.Observer;
import com.dougkeen.util.WakeLocker; import com.dougkeen.util.WakeLocker;
@ -98,6 +95,8 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
final Uri uri = mUri; final Uri uri = mUri;
final BartRunnerApplication bartRunnerApplication = (BartRunnerApplication) getApplication();
if (savedInstanceState != null if (savedInstanceState != null
&& savedInstanceState.containsKey("origin") && savedInstanceState.containsKey("origin")
&& savedInstanceState.containsKey("destination")) { && savedInstanceState.containsKey("destination")) {
@ -166,41 +165,41 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
mSelectedDeparture = (Departure) savedInstanceState mSelectedDeparture = (Departure) savedInstanceState
.getParcelable("selectedDeparture"); .getParcelable("selectedDeparture");
} }
if (savedInstanceState.getBoolean("hasActionMode") if (savedInstanceState.getBoolean("hasDepartureActionMode")
&& mSelectedDeparture != null) { && mSelectedDeparture != null) {
startDepartureActionMode(); startDepartureActionMode();
} }
if (savedInstanceState.getBoolean("hasYourTrainActionMode")
&& mSelectedDeparture != null) {
((Checkable) findViewById(R.id.yourTrainSection))
.setChecked(true);
startYourTrainActionMode(bartRunnerApplication);
}
} }
setListAdapter(mDeparturesAdapter); setListAdapter(mDeparturesAdapter);
final ListView listView = getListView(); final ListView listView = getListView();
listView.setEmptyView(findViewById(android.R.id.empty)); listView.setEmptyView(findViewById(android.R.id.empty));
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { listView.setOnItemClickListener(mListItemClickListener);
@Override listView.setOnItemLongClickListener(mListItemLongClickListener);
public void onItemClick(AdapterView<?> adapterView, View view,
int position, long id) {
mSelectedDeparture = (Departure) getListAdapter().getItem(
position);
view.setSelected(true);
startDepartureActionMode();
}
});
findViewById(R.id.missingDepartureText).setVisibility(View.VISIBLE); findViewById(R.id.missingDepartureText).setVisibility(View.VISIBLE);
findViewById(R.id.yourTrainSection).setOnClickListener(
mYourTrainSectionClickListener);
refreshBoardedDeparture(); refreshBoardedDeparture();
getSupportActionBar().setHomeButtonEnabled(true); getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final BartRunnerApplication bartRunnerApplication = (BartRunnerApplication) getApplication();
if (bartRunnerApplication.shouldPlayAlarmRingtone()) { if (bartRunnerApplication.shouldPlayAlarmRingtone()) {
soundTheAlarm(); soundTheAlarm();
} }
if (bartRunnerApplication.isAlarmSounding()) { if (bartRunnerApplication.isAlarmSounding()) {
Builder builder = new AlertDialog.Builder(this); Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.train_alert_text) builder.setMessage(R.string.train_alarm_text)
.setCancelable(false) .setCancelable(false)
.setNeutralButton(R.string.silence_alarm, .setNeutralButton(R.string.silence_alarm,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
@ -217,19 +216,19 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
private void soundTheAlarm() { private void soundTheAlarm() {
final BartRunnerApplication application = (BartRunnerApplication) getApplication(); final BartRunnerApplication application = (BartRunnerApplication) getApplication();
Uri alertSound = RingtoneManager Uri alarmSound = RingtoneManager
.getDefaultUri(RingtoneManager.TYPE_ALARM); .getDefaultUri(RingtoneManager.TYPE_ALARM);
if (alertSound == null || !tryToPlayRingtone(alertSound)) { if (alarmSound == null || !tryToPlayRingtone(alarmSound)) {
alertSound = RingtoneManager alarmSound = RingtoneManager
.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
if (alertSound == null || !tryToPlayRingtone(alertSound)) { if (alarmSound == null || !tryToPlayRingtone(alarmSound)) {
alertSound = RingtoneManager alarmSound = RingtoneManager
.getDefaultUri(RingtoneManager.TYPE_RINGTONE); .getDefaultUri(RingtoneManager.TYPE_RINGTONE);
} }
} }
if (application.getAlarmMediaPlayer() == null) { if (application.getAlarmMediaPlayer() == null) {
tryToPlayRingtone(alertSound); tryToPlayRingtone(alarmSound);
} }
mHandler.postDelayed(new Runnable() { mHandler.postDelayed(new Runnable() {
@Override @Override
@ -296,7 +295,54 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
} }
}; };
private Observer<Boolean> mAlarmPendingObserver; private boolean mWasLongClick = false;
private final AdapterView.OnItemClickListener mListItemClickListener = new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view,
int position, long id) {
if (mWasLongClick) {
mWasLongClick = false;
return;
}
if (mActionMode != null) {
/*
* If action mode is displayed, cancel out of that
*/
mActionMode.finish();
getListView().clearChoices();
} else {
/*
* Otherwise select the clicked departure as the one the user
* wants to board
*/
mSelectedDeparture = (Departure) getListAdapter().getItem(
position);
setBoardedDeparture(mSelectedDeparture);
}
}
};
private final AdapterView.OnItemLongClickListener mListItemLongClickListener = new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view,
int position, long id) {
mWasLongClick = true;
mSelectedDeparture = (Departure) getListAdapter().getItem(position);
view.setSelected(true);
startDepartureActionMode();
return false;
}
};
private final View.OnClickListener mYourTrainSectionClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
((Checkable) v).setChecked(true);
startYourTrainActionMode((BartRunnerApplication) getApplication());
}
};
protected DepartureArrayAdapter getListAdapter() { protected DepartureArrayAdapter getListAdapter() {
return mDeparturesAdapter; return mDeparturesAdapter;
@ -312,10 +358,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
super.onStop(); super.onStop();
if (mEtdService != null) if (mEtdService != null)
mEtdService.unregisterListener(this); mEtdService.unregisterListener(this);
if (mAlarmPendingObserver != null)
((BartRunnerApplication) getApplication())
.getAlarmPendingObservable().unregisterObserver(
mAlarmPendingObserver);
if (mBound) if (mBound)
unbindService(mConnection); unbindService(mConnection);
Ticker.getInstance().stopTicking(this); Ticker.getInstance().stopTicking(this);
@ -337,7 +379,10 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
} }
outState.putParcelableArray("departures", departures); outState.putParcelableArray("departures", departures);
outState.putParcelable("selectedDeparture", mSelectedDeparture); outState.putParcelable("selectedDeparture", mSelectedDeparture);
outState.putBoolean("hasActionMode", mActionMode != null); outState.putBoolean("hasDepartureActionMode",
isDepartureActionModeActive());
outState.putBoolean("hasYourTrainActionMode",
isYourTrainActionModeActive());
outState.putString("origin", mOrigin.abbreviation); outState.putString("origin", mOrigin.abbreviation);
outState.putString("destination", mDestination.abbreviation); outState.putString("destination", mDestination.abbreviation);
} }
@ -366,25 +411,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getSupportMenuInflater(); MenuInflater inflater = getSupportMenuInflater();
inflater.inflate(R.menu.route_menu, menu); inflater.inflate(R.menu.route_menu, menu);
final MenuItem cancelAlarmButton = menu
.findItem(R.id.cancel_alarm_button);
final BartRunnerApplication application = (BartRunnerApplication) getApplication();
if (application.isAlarmPending()) {
cancelAlarmButton.setVisible(true);
}
mAlarmPendingObserver = new Observer<Boolean>() {
@Override
public void onUpdate(final Boolean newValue) {
runOnUiThread(new Runnable() {
@Override
public void run() {
cancelAlarmButton.setVisible(newValue);
}
});
}
};
application.getAlarmPendingObservable().registerObserver(
mAlarmPendingObserver);
return true; return true;
} }
@ -397,11 +423,6 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent); startActivity(intent);
return true; return true;
} else if (itemId == R.id.cancel_alarm_button) {
Intent intent = new Intent(this, NotificationService.class);
intent.putExtra("cancelNotifications", true);
startService(intent);
return true;
} else if (itemId == R.id.view_on_bart_site_button) { } else if (itemId == R.id.view_on_bart_site_button) {
startActivity(new Intent( startActivity(new Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
@ -424,7 +445,7 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
private void refreshBoardedDeparture() { private void refreshBoardedDeparture() {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
final View yourTrainSection = findViewById(R.id.yourTrainSection); final YourTrainLayout yourTrainSection = (YourTrainLayout) findViewById(R.id.yourTrainSection);
int currentVisibility = yourTrainSection.getVisibility(); int currentVisibility = yourTrainSection.getVisibility();
final boolean boardedDepartureDoesNotApply = boardedDeparture == null final boolean boardedDepartureDoesNotApply = boardedDeparture == null
@ -433,65 +454,43 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
if (boardedDepartureDoesNotApply) { if (boardedDepartureDoesNotApply) {
if (currentVisibility != View.GONE) { if (currentVisibility != View.GONE) {
yourTrainSection.setVisibility(View.GONE); hideYourTrainSection();
} }
return; return;
} }
((TextView) findViewById(R.id.yourTrainDestinationText)) yourTrainSection.updateFromDeparture(boardedDeparture);
.setText(boardedDeparture.getTrainDestination().toString());
((TextView) findViewById(R.id.yourTrainTrainLengthText))
.setText(boardedDeparture.getTrainLengthText());
ImageView colorBar = (ImageView) findViewById(R.id.yourTrainDestinationColorBar);
((GradientDrawable) colorBar.getDrawable()).setColor(Color
.parseColor(boardedDeparture.getTrainDestinationColor()));
if (boardedDeparture.isBikeAllowed()) {
((ImageView) findViewById(R.id.yourTrainBikeIcon))
.setVisibility(View.VISIBLE);
} else {
((ImageView) findViewById(R.id.yourTrainBikeIcon))
.setVisibility(View.INVISIBLE);
}
if (boardedDeparture.getRequiresTransfer()) {
((ImageView) findViewById(R.id.yourTrainXferIcon))
.setVisibility(View.VISIBLE);
} else {
((ImageView) findViewById(R.id.yourTrainXferIcon))
.setVisibility(View.INVISIBLE);
}
CountdownTextView departureCountdown = (CountdownTextView) findViewById(R.id.yourTrainDepartureCountdown);
CountdownTextView arrivalCountdown = (CountdownTextView) findViewById(R.id.yourTrainArrivalCountdown);
departureCountdown.setText("Leaves in "
+ boardedDeparture.getCountdownText() + " "
+ boardedDeparture.getUncertaintyText());
departureCountdown.setTextProvider(new TextProvider() {
@Override
public String getText(long tickNumber) {
if (boardedDeparture.hasDeparted()) {
return "Departed";
} else {
return "Leaves in " + boardedDeparture.getCountdownText()
+ " " + boardedDeparture.getUncertaintyText();
}
}
});
arrivalCountdown.setText(boardedDeparture
.getEstimatedArrivalMinutesLeftText(this));
arrivalCountdown.setTextProvider(new TextProvider() {
@Override
public String getText(long tickNumber) {
return boardedDeparture
.getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this);
}
});
if (currentVisibility != View.VISIBLE) { if (currentVisibility != View.VISIBLE) {
yourTrainSection.setVisibility(View.VISIBLE); showYourTrainSection(yourTrainSection);
} }
if (mActionMode == null) {
for (int i = getListAdapter().getCount() - 1; i >= 0; i--) {
if (getListAdapter().getItem(i).equals(boardedDeparture)) {
getListView().setSelection(i);
final Checkable listItem = (Checkable) getListView()
.getChildAt(i);
if (listItem != null) {
listItem.setChecked(true);
}
break;
}
}
getListView().requestLayout();
}
}
private void setBoardedDeparture(Departure selectedDeparture) {
final BartRunnerApplication application = (BartRunnerApplication) getApplication();
selectedDeparture.setPassengerDestination(mDestination);
application.setBoardedDeparture(selectedDeparture);
refreshBoardedDeparture();
// Start the notification service
startService(new Intent(ViewDeparturesActivity.this,
BoardedDepartureService.class));
} }
private void startDepartureActionMode() { private void startDepartureActionMode() {
@ -511,26 +510,14 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
((Checkable) findViewById(R.id.yourTrainSection)).setChecked(false);
return false; return false;
} }
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.boardTrain) { if (item.getItemId() == R.id.boardTrain) {
final BartRunnerApplication application = (BartRunnerApplication) getApplication(); setBoardedDeparture(mSelectedDeparture);
mSelectedDeparture.setPassengerDestination(mDestination);
application.setBoardedDeparture(mSelectedDeparture);
refreshBoardedDeparture();
// Start the notification service
startService(new Intent(ViewDeparturesActivity.this,
NotificationService.class));
// Don't prompt for alert if train is about to leave
if (mSelectedDeparture.getMeanSecondsLeft() / 60 > 1) {
new TrainAlertDialogFragment().show(
getSupportFragmentManager(), "dialog");
}
mode.finish(); mode.finish();
return true; return true;
@ -547,6 +534,158 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
} }
private void startYourTrainActionMode(BartRunnerApplication application) {
if (mActionMode == null)
mActionMode = startActionMode(new YourTrainActionMode());
mActionMode.setTitle(R.string.your_train);
if (application.getBoardedDeparture() != null
&& application.getBoardedDeparture().isAlarmPending()) {
int leadTime = application.getBoardedDeparture()
.getAlarmLeadTimeMinutes();
mActionMode.setSubtitle(getAlarmSubtitle(leadTime));
} else {
mActionMode.setSubtitle(null);
}
}
private String getAlarmSubtitle(int leadTime) {
if (leadTime == 0)
return null;
return "Alarm " + leadTime + " minute" + (leadTime != 1 ? "s" : "")
+ " before departure";
}
private class YourTrainActionMode implements ActionMode.Callback {
private Observer<Boolean> mAlarmPendingObserver;
private Observer<Integer> mAlarmLeadTimeObserver;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Suppress new "your train" selections
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
mode.getMenuInflater()
.inflate(R.menu.your_train_context_menu, menu);
final MenuItem cancelAlarmButton = menu
.findItem(R.id.cancel_alarm_button);
final MenuItem setAlarmButton = menu
.findItem(R.id.set_alarm_button);
final BartRunnerApplication application = (BartRunnerApplication) getApplication();
final Departure boardedDeparture = application
.getBoardedDeparture();
if (boardedDeparture.isAlarmPending()) {
cancelAlarmButton.setVisible(true);
setAlarmButton.setIcon(R.drawable.ic_action_alarm);
} else if (boardedDeparture.getMeanSecondsLeft() > 60) {
setAlarmButton.setIcon(R.drawable.ic_action_add_alarm);
}
// Don't allow alarm setting if train is about to leave
if (boardedDeparture.getMeanSecondsLeft() / 60 < 1) {
menu.findItem(R.id.set_alarm_button).setVisible(false);
}
mAlarmPendingObserver = new Observer<Boolean>() {
@Override
public void onUpdate(final Boolean newValue) {
runOnUiThread(new Runnable() {
@Override
public void run() {
cancelAlarmButton.setVisible(newValue);
if (newValue) {
mActionMode
.setSubtitle(getAlarmSubtitle(boardedDeparture
.getAlarmLeadTimeMinutes()));
setAlarmButton
.setIcon(R.drawable.ic_action_alarm);
} else {
mActionMode.setSubtitle(null);
setAlarmButton
.setIcon(R.drawable.ic_action_add_alarm);
}
}
});
}
};
mAlarmLeadTimeObserver = new Observer<Integer>() {
@Override
public void onUpdate(final Integer newValue) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mActionMode.setSubtitle(getAlarmSubtitle(newValue));
}
});
}
};
boardedDeparture.getAlarmPendingObservable().registerObserver(
mAlarmPendingObserver);
boardedDeparture.getAlarmLeadTimeMinutesObservable()
.registerObserver(mAlarmLeadTimeObserver);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
getListView().clearChoices();
getListView().requestLayout();
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.set_alarm_button) {
BartRunnerApplication application = (BartRunnerApplication) getApplication();
// Don't prompt for alarm if train is about to leave
if (application.getBoardedDeparture().getMeanSecondsLeft() > 60) {
new TrainAlarmDialogFragment().show(
getSupportFragmentManager(), "dialog");
}
return true;
} else if (itemId == R.id.cancel_alarm_button) {
Intent intent = new Intent(ViewDeparturesActivity.this,
BoardedDepartureService.class);
intent.putExtra("cancelNotifications", true);
startService(intent);
return true;
} else if (itemId == R.id.delete) {
Intent intent = new Intent(ViewDeparturesActivity.this,
BoardedDepartureService.class);
intent.putExtra("clearBoardedDeparture", true);
startService(intent);
hideYourTrainSection();
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
((Checkable) findViewById(R.id.yourTrainSection)).setChecked(false);
final BartRunnerApplication application = (BartRunnerApplication) getApplication();
final Departure boardedDeparture = application
.getBoardedDeparture();
if (boardedDeparture != null) {
boardedDeparture.getAlarmPendingObservable()
.unregisterObserver(mAlarmPendingObserver);
boardedDeparture.getAlarmLeadTimeMinutesObservable()
.unregisterObserver(mAlarmLeadTimeObserver);
}
mAlarmPendingObserver = null;
mAlarmLeadTimeObserver = null;
mActionMode = null;
// Enable new "your train" selections
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
}
@Override @Override
public void onETDChanged(final List<Departure> departures) { public void onETDChanged(final List<Departure> departures) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@ -558,6 +697,10 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
mProgress.setVisibility(View.INVISIBLE); mProgress.setVisibility(View.INVISIBLE);
Linkify.addLinks(textView, Linkify.WEB_URLS); Linkify.addLinks(textView, Linkify.WEB_URLS);
} else { } else {
// TODO: Figure out why Ticker occasionally stops
Ticker.getInstance().startTicking(
ViewDeparturesActivity.this);
// Merge lists // Merge lists
if (mDeparturesAdapter.getCount() > 0) { if (mDeparturesAdapter.getCount() > 0) {
int adapterIndex = -1; int adapterIndex = -1;
@ -639,4 +782,24 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
return null; return null;
return new StationPair(mOrigin, mDestination); return new StationPair(mOrigin, mDestination);
} }
private void hideYourTrainSection() {
findViewById(R.id.yourTrainSection).setVisibility(View.GONE);
}
private void showYourTrainSection(final YourTrainLayout yourTrainSection) {
yourTrainSection.setVisibility(View.VISIBLE);
}
private boolean isYourTrainActionModeActive() {
return mActionMode != null
&& mActionMode.getTitle()
.equals(getString(R.string.your_train));
}
private boolean isDepartureActionModeActive() {
return mActionMode != null
&& !mActionMode.getTitle().equals(
getString(R.string.your_train));
}
} }

View File

@ -1,12 +1,12 @@
package com.dougkeen.bart.controls; package com.dougkeen.bart.controls;
import com.dougkeen.bart.R;
import android.content.Context; import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.widget.Checkable; import android.widget.Checkable;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import com.dougkeen.bart.R;
public class DepartureListItemLayout extends RelativeLayout implements public class DepartureListItemLayout extends RelativeLayout implements
Checkable { Checkable {

View File

@ -2,12 +2,12 @@ package com.dougkeen.bart.controls;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.dougkeen.bart.model.TextProvider;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.TextSwitcher; import android.widget.TextSwitcher;
import com.dougkeen.bart.model.TextProvider;
public class TimedTextSwitcher extends TextSwitcher implements public class TimedTextSwitcher extends TextSwitcher implements
Ticker.TickSubscriber { Ticker.TickSubscriber {

View File

@ -0,0 +1,120 @@
package com.dougkeen.bart.controls;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Checkable;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.dougkeen.bart.R;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.TextProvider;
public class YourTrainLayout extends RelativeLayout implements Checkable {
public YourTrainLayout(Context context) {
super(context);
assignLayout(context);
}
public YourTrainLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
assignLayout(context);
}
public YourTrainLayout(Context context, AttributeSet attrs) {
super(context, attrs);
assignLayout(context);
}
public void assignLayout(Context context) {
LayoutInflater.from(context).inflate(R.layout.your_train, this, true);
}
private boolean mChecked;
@Override
public boolean isChecked() {
return mChecked;
}
@Override
public void setChecked(boolean checked) {
mChecked = checked;
setBackground();
}
private void setBackground() {
if (isChecked()) {
setBackgroundDrawable(getContext().getResources().getDrawable(
R.color.blue_selection));
} else {
setBackgroundDrawable(getContext().getResources().getDrawable(
R.color.gray));
}
}
@Override
public void toggle() {
setChecked(!isChecked());
}
public void updateFromDeparture(final Departure boardedDeparture) {
((TextView) findViewById(R.id.yourTrainDestinationText))
.setText(boardedDeparture.getTrainDestination().toString());
((TextView) findViewById(R.id.yourTrainTrainLengthText))
.setText(boardedDeparture.getTrainLengthText());
ImageView colorBar = (ImageView) findViewById(R.id.yourTrainDestinationColorBar);
((GradientDrawable) colorBar.getDrawable()).setColor(Color
.parseColor(boardedDeparture.getTrainDestinationColor()));
if (boardedDeparture.isBikeAllowed()) {
((ImageView) findViewById(R.id.yourTrainBikeIcon))
.setVisibility(View.VISIBLE);
} else {
((ImageView) findViewById(R.id.yourTrainBikeIcon))
.setVisibility(View.INVISIBLE);
}
if (boardedDeparture.getRequiresTransfer()) {
((ImageView) findViewById(R.id.yourTrainXferIcon))
.setVisibility(View.VISIBLE);
} else {
((ImageView) findViewById(R.id.yourTrainXferIcon))
.setVisibility(View.INVISIBLE);
}
CountdownTextView departureCountdown = (CountdownTextView) findViewById(R.id.yourTrainDepartureCountdown);
CountdownTextView arrivalCountdown = (CountdownTextView) findViewById(R.id.yourTrainArrivalCountdown);
final TextProvider textProvider = new TextProvider() {
@Override
public String getText(long tickNumber) {
if (boardedDeparture.hasDeparted()) {
return "Departed";
} else {
return "Leaves in " + boardedDeparture.getCountdownText()
+ " " + boardedDeparture.getUncertaintyText();
}
}
};
departureCountdown.setText(textProvider.getText(0));
departureCountdown.setTextProvider(textProvider);
arrivalCountdown.setText(boardedDeparture
.getEstimatedArrivalMinutesLeftText(getContext()));
arrivalCountdown.setTextProvider(new TextProvider() {
@Override
public String getText(long tickNumber) {
return boardedDeparture
.getEstimatedArrivalMinutesLeftText(getContext());
}
});
setBackground();
}
}

View File

@ -13,8 +13,6 @@ import android.view.ViewGroup;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextSwitcher; import android.widget.TextSwitcher;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ViewSwitcher.ViewFactory; import android.widget.ViewSwitcher.ViewFactory;

View File

@ -1,5 +1,6 @@
package com.dougkeen.bart.data; package com.dougkeen.bart.data;
public enum RoutesColumns { public enum RoutesColumns {
_ID("_id", "INTEGER", false), _ID("_id", "INTEGER", false),
FROM_STATION("FROM_STATION", "TEXT", false), FROM_STATION("FROM_STATION", "TEXT", false),

View File

@ -18,10 +18,12 @@ import android.text.format.DateFormat;
import android.util.Log; import android.util.Log;
import com.dougkeen.bart.R; import com.dougkeen.bart.R;
import com.dougkeen.bart.services.NotificationService; import com.dougkeen.bart.services.BoardedDepartureService;
import com.dougkeen.util.Observable;
public class Departure implements Parcelable, Comparable<Departure> { public class Departure implements Parcelable, Comparable<Departure> {
private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000; private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 5000;
private static final int EXPIRE_MINUTES_AFTER_ARRIVAL = 1;
public Departure() { public Departure() {
super(); super();
@ -67,8 +69,9 @@ public class Departure implements Parcelable, Comparable<Departure> {
private long arrivalTimeOverride; private long arrivalTimeOverride;
private int alarmLeadTimeMinutes; private Observable<Integer> alarmLeadTimeMinutes = new Observable<Integer>(
private boolean alarmPending; 0);
private Observable<Boolean> alarmPending = new Observable<Boolean>(false);
public Station getOrigin() { public Station getOrigin() {
return origin; return origin;
@ -308,7 +311,7 @@ public class Departure implements Parcelable, Comparable<Departure> {
} }
public boolean hasDeparted() { public boolean hasDeparted() {
return getMeanSecondsLeft() < 0; return getMeanSecondsLeft() <= 0;
} }
public void calculateEstimates(long originalEstimateTime) { public void calculateEstimates(long originalEstimateTime) {
@ -333,40 +336,45 @@ public class Departure implements Parcelable, Comparable<Departure> {
setEstimatedTripTime(departure.getEstimatedTripTime()); setEstimatedTripTime(departure.getEstimatedTripTime());
} }
long newMin = Math.max(getMinEstimate(), departure.getMinEstimate());
long newMax = Math.min(getMaxEstimate(), departure.getMaxEstimate());
if ((getMaxEstimate() - departure.getMinEstimate()) < MINIMUM_MERGE_OVERLAP_MILLIS if ((getMaxEstimate() - departure.getMinEstimate()) < MINIMUM_MERGE_OVERLAP_MILLIS
|| departure.getMaxEstimate() - getMinEstimate() < MINIMUM_MERGE_OVERLAP_MILLIS) { || departure.getMaxEstimate() - getMinEstimate() < MINIMUM_MERGE_OVERLAP_MILLIS) {
/* /*
* The estimate must have changed... just use the latest incoming * The estimate must have changed... just use the latest incoming
* values * values
*/ */
setMinEstimate(departure.getMinEstimate()); newMin = departure.getMinEstimate();
setMaxEstimate(departure.getMaxEstimate()); newMax = departure.getMaxEstimate();
return;
} }
final long newMin = Math.max(getMinEstimate(),
departure.getMinEstimate());
final long newMax = Math.min(getMaxEstimate(),
departure.getMaxEstimate());
/* /*
* If the new departure would mark this as departed, and we have < 1 * If the new departure would mark this as departed, and we have < 60
* minute left on a fairly accurate local estimate, ignore the incoming * seconds left on a fairly accurate local estimate, ignore the incoming
* departure * departure
*/ */
if (!wasDeparted && getMeanSecondsLeft(newMin, newMax) < 0 if (!wasDeparted && getMeanSecondsLeft(newMin, newMax) <= 0
&& getMeanSecondsLeft() < 60 && getUncertaintySeconds() < 30) { && getMeanSecondsLeft() < 60 && getUncertaintySeconds() < 30) {
Log.d(Constants.TAG, Log.d(Constants.TAG,
"Skipping estimate merge, since it would make this departure show as 'departed' prematurely"); "Skipping estimate merge, since it would make this departure show as 'departed' prematurely");
return; return;
} }
if (newMax > newMin) { // We can never have 0 or negative uncertainty if (newMax > newMin) {
// We must never have 0 or negative uncertainty
setMinEstimate(newMin); setMinEstimate(newMin);
setMaxEstimate(newMax); setMaxEstimate(newMax);
} }
} }
public boolean hasExpired() {
final long now = System.currentTimeMillis();
return getMaxEstimate() < now
&& getEstimatedArrivalTime() + EXPIRE_MINUTES_AFTER_ARRIVAL
* 60000 < now;
}
public int compareTo(Departure another) { public int compareTo(Departure another) {
return (this.getMeanSecondsLeft() > another.getMeanSecondsLeft()) ? 1 return (this.getMeanSecondsLeft() > another.getMeanSecondsLeft()) ? 1
: ((this.getMeanSecondsLeft() == another.getMeanSecondsLeft()) ? 0 : ((this.getMeanSecondsLeft() == another.getMeanSecondsLeft()) ? 0
@ -475,10 +483,18 @@ public class Departure implements Parcelable, Comparable<Departure> {
} }
public int getAlarmLeadTimeMinutes() { public int getAlarmLeadTimeMinutes() {
return alarmLeadTimeMinutes.getValue();
}
public Observable<Integer> getAlarmLeadTimeMinutesObservable() {
return alarmLeadTimeMinutes; return alarmLeadTimeMinutes;
} }
public boolean isAlarmPending() { public boolean isAlarmPending() {
return alarmPending.getValue();
}
public Observable<Boolean> getAlarmPendingObservable() {
return alarmPending; return alarmPending;
} }
@ -489,7 +505,7 @@ public class Departure implements Parcelable, Comparable<Departure> {
} }
private long getAlarmClockTime() { private long getAlarmClockTime() {
return getMeanEstimate() - alarmLeadTimeMinutes * 60 * 1000; return getMeanEstimate() - alarmLeadTimeMinutes.getValue() * 60 * 1000;
} }
public int getSecondsUntilAlarm() { public int getSecondsUntilAlarm() {
@ -497,8 +513,8 @@ public class Departure implements Parcelable, Comparable<Departure> {
} }
public void setUpAlarm(int leadTimeMinutes) { public void setUpAlarm(int leadTimeMinutes) {
this.alarmLeadTimeMinutes = leadTimeMinutes; this.alarmLeadTimeMinutes.setValue(leadTimeMinutes);
this.alarmPending = true; this.alarmPending.setValue(true);
} }
public void updateAlarm(Context context, AlarmManager alarmManager) { public void updateAlarm(Context context, AlarmManager alarmManager) {
@ -509,20 +525,20 @@ public class Departure implements Parcelable, Comparable<Departure> {
final PendingIntent alarmIntent = getAlarmIntent(context); final PendingIntent alarmIntent = getAlarmIntent(context);
alarmManager.cancel(alarmIntent); alarmManager.cancel(alarmIntent);
long alertTime = getAlarmClockTime(); long alarmTime = getAlarmClockTime();
alarmManager.set(AlarmManager.RTC_WAKEUP, alertTime, alarmIntent); alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, alarmIntent);
if (Log.isLoggable(Constants.TAG, Log.VERBOSE)) if (Log.isLoggable(Constants.TAG, Log.VERBOSE))
Log.v(Constants.TAG, Log.v(Constants.TAG,
"Scheduling alarm for " "Scheduling alarm for "
+ DateFormatUtils.format(alertTime, "h:mm:ss")); + DateFormatUtils.format(alarmTime, "h:mm:ss"));
} }
} }
public void cancelAlarm(Context context, AlarmManager alarmManager) { public void cancelAlarm(Context context, AlarmManager alarmManager) {
alarmManager.cancel(getAlarmIntent(context)); alarmManager.cancel(getAlarmIntent(context));
this.alarmPending = false; this.alarmPending.setValue(false);
} }
private PendingIntent notificationIntent; private PendingIntent notificationIntent;
@ -546,7 +562,7 @@ public class Departure implements Parcelable, Comparable<Departure> {
: "")); : ""));
final Intent cancelAlarmIntent = new Intent(context, final Intent cancelAlarmIntent = new Intent(context,
NotificationService.class); BoardedDepartureService.class);
cancelAlarmIntent.putExtra("cancelNotifications", true); cancelAlarmIntent.putExtra("cancelNotifications", true);
Builder notificationBuilder = new NotificationCompat.Builder(context) Builder notificationBuilder = new NotificationCompat.Builder(context)
.setOngoing(true) .setOngoing(true)
@ -564,7 +580,7 @@ public class Departure implements Parcelable, Comparable<Departure> {
"Cancel alarm", "Cancel alarm",
PendingIntent.getService(context, 0, cancelAlarmIntent, PendingIntent.getService(context, 0, cancelAlarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT)).setSubText( PendingIntent.FLAG_UPDATE_CURRENT)).setSubText(
"Alert " + getAlarmLeadTimeMinutes() "Alarm " + getAlarmLeadTimeMinutes()
+ " minutes before departure"); + " minutes before departure");
} }
} else if (isAlarmPending()) { } else if (isAlarmPending()) {
@ -655,6 +671,6 @@ public class Departure implements Parcelable, Comparable<Departure> {
}; };
public void notifyAlarmHasBeenHandled() { public void notifyAlarmHasBeenHandled() {
this.alarmPending = false; this.alarmPending.setValue(false);
} }
} }

View File

@ -2,14 +2,14 @@ package com.dougkeen.bart.model;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import com.dougkeen.bart.data.CursorUtils;
import com.dougkeen.bart.data.RoutesColumns;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.dougkeen.bart.data.CursorUtils;
import com.dougkeen.bart.data.RoutesColumns;
public class StationPair implements Parcelable { public class StationPair implements Parcelable {
public StationPair(Station origin, Station destination) { public StationPair(Station origin, Station destination) {
super(); super();

View File

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

View File

@ -12,13 +12,13 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Station;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.util.Log; import android.util.Log;
import android.util.Xml; import android.util.Xml;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Station;
public abstract class GetRouteFareTask extends public abstract class GetRouteFareTask extends
AsyncTask<GetRouteFareTask.Params, Integer, String> { AsyncTask<GetRouteFareTask.Params, Integer, String> {

View File

@ -1,38 +1,30 @@
package com.dougkeen.bart.services; package com.dougkeen.bart.services;
import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.time.DateFormatUtils;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.util.Log;
import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.R;
import com.dougkeen.bart.model.Constants;
import com.dougkeen.bart.model.Departure; import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.StationPair; import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.services.EtdService.EtdServiceBinder; import com.dougkeen.bart.services.EtdService.EtdServiceBinder;
import com.dougkeen.bart.services.EtdService.EtdServiceListener; import com.dougkeen.bart.services.EtdService.EtdServiceListener;
import com.dougkeen.util.Observer;
public class NotificationService extends Service implements EtdServiceListener { public class BoardedDepartureService extends Service implements
EtdServiceListener {
private static final int DEPARTURE_NOTIFICATION_ID = 123; private static final int DEPARTURE_NOTIFICATION_ID = 123;
@ -44,22 +36,29 @@ public class NotificationService extends Service implements EtdServiceListener {
private StationPair mStationPair; private StationPair mStationPair;
private NotificationManager mNotificationManager; private NotificationManager mNotificationManager;
private AlarmManager mAlarmManager; private AlarmManager mAlarmManager;
private PendingIntent mNotificationIntent;
private Handler mHandler; private Handler mHandler;
private boolean mHasShutDown = false; private boolean mHasShutDown = false;
public NotificationService() { public BoardedDepartureService() {
super(); super();
} }
private final class ServiceHandler extends Handler { private static final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) { private final WeakReference<BoardedDepartureService> mServiceRef;
public ServiceHandler(Looper looper,
BoardedDepartureService boardedDepartureService) {
super(looper); super(looper);
mServiceRef = new WeakReference<BoardedDepartureService>(
boardedDepartureService);
} }
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
onHandleIntent((Intent) msg.obj); BoardedDepartureService service = mServiceRef.get();
if (service != null) {
service.onHandleIntent((Intent) msg.obj);
}
} }
} }
@ -74,7 +73,8 @@ public class NotificationService extends Service implements EtdServiceListener {
public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceConnected(ComponentName name, IBinder service) {
mEtdService = ((EtdServiceBinder) service).getService(); mEtdService = ((EtdServiceBinder) service).getService();
if (getStationPair() != null) { if (getStationPair() != null) {
mEtdService.registerListener(NotificationService.this, false); mEtdService.registerListener(BoardedDepartureService.this,
false);
} }
mBound = true; mBound = true;
} }
@ -87,7 +87,7 @@ public class NotificationService extends Service implements EtdServiceListener {
thread.start(); thread.start();
mServiceLooper = thread.getLooper(); mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper); mServiceHandler = new ServiceHandler(mServiceLooper, this);
bindService(new Intent(this, EtdService.class), mConnection, bindService(new Intent(this, EtdService.class), mConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
@ -122,17 +122,21 @@ public class NotificationService extends Service implements EtdServiceListener {
} }
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final BartRunnerApplication application = (BartRunnerApplication) getApplication();
.getBoardedDeparture(); final Departure boardedDeparture = application.getBoardedDeparture();
if (boardedDeparture == null) { if (boardedDeparture == null || intent == null) {
// Nothing to notify about // Nothing to notify about
return; return;
} }
if (intent.getBooleanExtra("cancelNotifications", false)) { if (intent.getBooleanExtra("cancelNotifications", false)
// We want to cancel the alarm/notification || intent.getBooleanExtra("clearBoardedDeparture", false)) {
// We want to cancel the alarm
boardedDeparture boardedDeparture
.cancelAlarm(getApplicationContext(), mAlarmManager); .cancelAlarm(getApplicationContext(), mAlarmManager);
shutDown(false); if (intent.getBooleanExtra("clearBoardedDeparture", false)) {
application.setBoardedDeparture(null);
shutDown(false);
}
return; return;
} }
@ -148,11 +152,20 @@ public class NotificationService extends Service implements EtdServiceListener {
mEtdService.registerListener(this, false); mEtdService.registerListener(this, false);
} }
Intent targetIntent = new Intent(Intent.ACTION_VIEW, boardedDeparture.getAlarmLeadTimeMinutesObservable().registerObserver(
mStationPair.getUri()); new Observer<Integer>() {
targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @Override
mNotificationIntent = PendingIntent.getActivity(this, 0, targetIntent, public void onUpdate(Integer newValue) {
PendingIntent.FLAG_UPDATE_CURRENT); updateNotification();
}
});
boardedDeparture.getAlarmPendingObservable().registerObserver(
new Observer<Boolean>() {
@Override
public void onUpdate(Boolean newValue) {
updateNotification();
}
});
updateNotification(); updateNotification();
@ -179,6 +192,9 @@ public class NotificationService extends Service implements EtdServiceListener {
.getUncertaintySeconds() != departure .getUncertaintySeconds() != departure
.getUncertaintySeconds())) { .getUncertaintySeconds())) {
boardedDeparture.mergeEstimate(departure); boardedDeparture.mergeEstimate(departure);
// Also merge back, in case boardedDeparture estimate is better
departure.mergeEstimate(boardedDeparture);
updateAlarm(); updateAlarm();
break; break;
} }
@ -211,8 +227,9 @@ public class NotificationService extends Service implements EtdServiceListener {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
if (boardedDeparture.hasDeparted()) { if (boardedDeparture == null || boardedDeparture.hasDeparted()) {
shutDown(false); shutDown(false);
return;
} }
boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager); boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager);
@ -280,4 +297,5 @@ public class NotificationService extends Service implements EtdServiceListener {
// Doesn't support binding // Doesn't support binding
return null; return null;
} }
} }

View File

@ -36,6 +36,10 @@ public class Observable<T> {
listeners.remove(observer); listeners.remove(observer);
} }
public void unregisterAllObservers() {
listeners.clear();
}
protected void notifyOfChange(T value) { protected void notifyOfChange(T value) {
for (Observer<T> listener : listeners.keySet()) { for (Observer<T> listener : listeners.keySet()) {
if (listener != null) { if (listener != null) {