BartRunnerAndroid/src/com/dougkeen/bart/model/Departure.java
Doug Keen 5836ccb89c Added alarm indicator to Your Train card.
Fixed mysterious "freeze" of notifications (turned out to be unexpected gc of weak references).
Departure countdown now reads "Leaving" at the end of the countdown.
2012-10-10 17:21:30 -07:00

691 lines
19 KiB
Java

package com.dougkeen.bart.model;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.text.format.DateFormat;
import android.util.Log;
import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.R;
import com.dougkeen.bart.services.BoardedDepartureService;
import com.dougkeen.util.Observable;
public class Departure implements Parcelable, Comparable<Departure> {
private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 5000;
private static final int EXPIRE_MINUTES_AFTER_ARRIVAL = 1;
public Departure() {
super();
}
public Departure(String destinationAbbr, String destinationColor,
String platform, String direction, boolean bikeAllowed,
String trainLength, int minutes) {
super();
this.trainDestination = Station.getByAbbreviation(destinationAbbr);
this.destinationColor = destinationColor;
this.platform = platform;
this.direction = direction;
this.bikeAllowed = bikeAllowed;
this.trainLength = trainLength;
this.minutes = minutes;
}
public Departure(Parcel in) {
readFromParcel(in);
}
private Station origin;
private Station trainDestination;
private Station passengerDestination;
private Line line;
private String destinationColor;
private String platform;
private String direction;
private boolean bikeAllowed;
private String trainLength;
private boolean requiresTransfer;
private boolean transferScheduled;
private int minutes;
private long minEstimate;
private long maxEstimate;
private int estimatedTripTime;
private boolean beganAsDeparted;
private long arrivalTimeOverride;
private Observable<Integer> alarmLeadTimeMinutes = new Observable<Integer>(
0);
private Observable<Boolean> alarmPending = new Observable<Boolean>(false);
private boolean listedInETDs = true;
public Station getOrigin() {
return origin;
}
public void setOrigin(Station origin) {
this.origin = origin;
}
public Station getTrainDestination() {
return trainDestination;
}
public void setTrainDestination(Station destination) {
this.trainDestination = destination;
}
public String getTrainDestinationName() {
if (trainDestination != null)
return trainDestination.name;
return null;
}
public String getTrainDestinationAbbreviation() {
if (trainDestination != null)
return trainDestination.abbreviation;
return null;
}
public Station getPassengerDestination() {
return passengerDestination;
}
public void setPassengerDestination(Station passengerDestination) {
this.passengerDestination = passengerDestination;
}
public StationPair getStationPair() {
if (passengerDestination != null) {
return new StationPair(origin, passengerDestination);
} else {
return null;
}
}
public Line getLine() {
return line;
}
public void setLine(Line line) {
this.line = line;
}
public String getTrainDestinationColor() {
return destinationColor;
}
public void setTrainDestinationColor(String destinationColor) {
this.destinationColor = destinationColor;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getDirection() {
return direction;
}
public void setDirection(String direction) {
this.direction = direction;
}
public boolean isBikeAllowed() {
return bikeAllowed;
}
public void setBikeAllowed(boolean bikeAllowed) {
this.bikeAllowed = bikeAllowed;
}
public String getTrainLength() {
return trainLength;
}
public void setTrainLength(String trainLength) {
this.trainLength = trainLength;
}
public String getTrainLengthText() {
return trainLength + " car train";
}
public boolean getRequiresTransfer() {
return requiresTransfer;
}
public void setRequiresTransfer(boolean requiresTransfer) {
this.requiresTransfer = requiresTransfer;
}
public boolean isTransferScheduled() {
return transferScheduled;
}
public void setTransferScheduled(boolean transferScheduled) {
this.transferScheduled = transferScheduled;
}
public int getMinutes() {
return minutes;
}
public void setMinutes(int minutes) {
this.minutes = minutes;
if (minutes == 0) {
beganAsDeparted = true;
}
}
public long getMinEstimate() {
return minEstimate;
}
public void setMinEstimate(long minEstimate) {
this.minEstimate = minEstimate;
}
public long getMaxEstimate() {
return maxEstimate;
}
public void setMaxEstimate(long maxEstimate) {
this.maxEstimate = maxEstimate;
}
public int getEstimatedTripTime() {
return estimatedTripTime;
}
public void setEstimatedTripTime(int estimatedTripTime) {
this.estimatedTripTime = estimatedTripTime;
}
public boolean hasEstimatedTripTime() {
return this.estimatedTripTime > 0;
}
public boolean hasAnyArrivalEstimate() {
return this.estimatedTripTime > 0 || this.arrivalTimeOverride > 0;
}
public int getUncertaintySeconds() {
return (int) (maxEstimate - minEstimate + 1000) / 2000;
}
public int getMinSecondsLeft() {
return (int) ((getMinEstimate() - System.currentTimeMillis()) / 1000);
}
public int getMaxSecondsLeft() {
return (int) ((getMaxEstimate() - System.currentTimeMillis()) / 1000);
}
public int getMeanSecondsLeft() {
return (int) getMeanSecondsLeft(getMinEstimate(), getMaxEstimate());
}
public int getMeanSecondsLeft(long min, long max) {
return (int) ((getMeanEstimate(min, max) - System.currentTimeMillis()) / 1000);
}
public long getMeanEstimate() {
return getMeanEstimate(getMinEstimate(), getMaxEstimate());
}
public long getMeanEstimate(long min, long max) {
return (min + max) / 2;
}
public long getArrivalTimeOverride() {
return arrivalTimeOverride;
}
public void setArrivalTimeOverride(long arrivalTimeOverride) {
this.arrivalTimeOverride = arrivalTimeOverride;
}
public long getEstimatedArrivalTime() {
if (arrivalTimeOverride > 0) {
return arrivalTimeOverride;
}
return getMeanEstimate() + getEstimatedTripTime();
}
public long getEstimatedArrivalMinutesLeft() {
long millisLeft = getEstimatedArrivalTime()
- System.currentTimeMillis();
if (millisLeft < 0) {
return -1;
} else {
// Add ~30s to emulate rounding
return (millisLeft + 29999) / (60 * 1000);
}
}
public String getEstimatedArrivalMinutesLeftText(Context context) {
if (!hasAnyArrivalEstimate()) {
return "Estimated arrival unknown";
}
long minutesLeft = getEstimatedArrivalMinutesLeft();
if (minutesLeft < 0) {
return "Arrived at destination";
} else if (minutesLeft == 0) {
return "Arrives ~" + getEstimatedArrivalTimeText(context)
+ " (<1 min)";
} else if (minutesLeft == 1) {
return "Arrives ~" + getEstimatedArrivalTimeText(context)
+ " (1 min)";
} else {
return "Arrives ~" + getEstimatedArrivalTimeText(context) + " ("
+ minutesLeft + " mins)";
}
}
public String getEstimatedArrivalTimeText(Context context) {
if (getEstimatedTripTime() > 0 || arrivalTimeOverride > 0) {
return DateFormat.getTimeFormat(context).format(
new Date(getEstimatedArrivalTime()));
} else {
return "";
}
}
public boolean hasDeparted() {
return getMeanSecondsLeft() <= 0;
}
public void calculateEstimates(long originalEstimateTime) {
setMinEstimate(originalEstimateTime + (getMinutes() * 60 * 1000)
- (30000));
setMaxEstimate(getMinEstimate() + 60000);
}
public void mergeEstimate(Departure departure) {
if (departure.hasDeparted() && origin.longStationLinger
&& getMinEstimate() > 0 && !beganAsDeparted) {
/*
* This is probably not a true departure, but an indication that the
* train is in the station. Don't update the estimates.
*/
return;
}
boolean wasDeparted = hasDeparted();
if (!hasAnyArrivalEstimate() && departure.hasAnyArrivalEstimate()) {
setArrivalTimeOverride(departure.getArrivalTimeOverride());
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
|| departure.getMaxEstimate() - getMinEstimate() < MINIMUM_MERGE_OVERLAP_MILLIS) {
/*
* The estimate must have changed... just use the latest incoming
* values
*/
newMin = departure.getMinEstimate();
newMax = departure.getMaxEstimate();
}
/*
* If the new departure would mark this as departed, and we have < 60
* seconds left on a fairly accurate local estimate, ignore the incoming
* departure
*/
if (!wasDeparted && getMeanSecondsLeft(newMin, newMax) <= 0
&& getMeanSecondsLeft() < 60 && getUncertaintySeconds() < 30) {
Log.d(Constants.TAG,
"Skipping estimate merge, since it would make this departure show as 'departed' prematurely");
return;
}
if (newMax > newMin) {
// We must never have 0 or negative uncertainty
setMinEstimate(newMin);
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) {
return (this.getMeanSecondsLeft() > another.getMeanSecondsLeft()) ? 1
: ((this.getMeanSecondsLeft() == another.getMeanSecondsLeft()) ? 0
: -1);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (bikeAllowed ? 1231 : 1237);
result = prime
* result
+ ((trainDestination == null) ? 0 : trainDestination.hashCode());
result = prime
* result
+ ((destinationColor == null) ? 0 : destinationColor.hashCode());
result = prime * result
+ ((direction == null) ? 0 : direction.hashCode());
result = prime * result + ((line == null) ? 0 : line.hashCode());
result = prime * result + (int) (maxEstimate ^ (maxEstimate >>> 32));
result = prime * result + (int) (minEstimate ^ (minEstimate >>> 32));
result = prime * result + minutes;
result = prime * result
+ ((platform == null) ? 0 : platform.hashCode());
result = prime * result + (requiresTransfer ? 1231 : 1237);
result = prime * result
+ ((trainLength == null) ? 0 : trainLength.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Departure other = (Departure) obj;
if (bikeAllowed != other.bikeAllowed)
return false;
if (trainDestination != other.trainDestination)
return false;
if (destinationColor == null) {
if (other.destinationColor != null)
return false;
} else if (!destinationColor.equals(other.destinationColor))
return false;
if (direction == null) {
if (other.direction != null)
return false;
} else if (!direction.equals(other.direction))
return false;
if (line != other.line)
return false;
if (Math.abs(maxEstimate - other.maxEstimate) > getEqualsTolerance())
return false;
if (platform == null) {
if (other.platform != null)
return false;
} else if (!platform.equals(other.platform))
return false;
if (requiresTransfer != other.requiresTransfer)
return false;
if (trainLength == null) {
if (other.trainLength != null)
return false;
} else if (!trainLength.equals(other.trainLength))
return false;
return true;
}
private int getEqualsTolerance() {
if (origin != null) {
return origin.departureEqualityTolerance;
} else {
return Station.DEFAULT_DEPARTURE_EQUALITY_TOLERANCE;
}
}
public String getCountdownText() {
StringBuilder builder = new StringBuilder();
int secondsLeft = getMeanSecondsLeft();
if (hasDeparted()) {
if (origin != null && origin.longStationLinger && beganAsDeparted) {
builder.append("At station");
} else if (isListedInETDs()) {
builder.append(BartRunnerApplication.getAppContext().getString(
R.string.leaving));
} else {
builder.append(BartRunnerApplication.getAppContext().getString(
R.string.departed));
}
} else {
builder.append(secondsLeft / 60);
builder.append("m, ");
builder.append(secondsLeft % 60);
builder.append("s");
}
return builder.toString();
}
public String getUncertaintyText() {
if (hasDeparted()) {
return "";
} else {
return "" + getUncertaintySeconds() + "s)";
}
}
public boolean isListedInETDs() {
return listedInETDs;
}
public void setListedInETDs(boolean listedInETDs) {
this.listedInETDs = listedInETDs;
}
public int getAlarmLeadTimeMinutes() {
return alarmLeadTimeMinutes.getValue();
}
public Observable<Integer> getAlarmLeadTimeMinutesObservable() {
return alarmLeadTimeMinutes;
}
public boolean isAlarmPending() {
return alarmPending.getValue();
}
public Observable<Boolean> getAlarmPendingObservable() {
return alarmPending;
}
private PendingIntent getAlarmIntent(Context context) {
return PendingIntent.getBroadcast(context, 0, new Intent(
Constants.ACTION_ALARM, getStationPair().getUri()),
PendingIntent.FLAG_UPDATE_CURRENT);
}
private long getAlarmClockTime() {
return getMeanEstimate() - alarmLeadTimeMinutes.getValue() * 60 * 1000;
}
public int getSecondsUntilAlarm() {
return getMeanSecondsLeft() - getAlarmLeadTimeMinutes() * 60;
}
public void setUpAlarm(int leadTimeMinutes) {
this.alarmLeadTimeMinutes.setValue(leadTimeMinutes);
this.alarmPending.setValue(true);
}
public void updateAlarm(Context context, AlarmManager alarmManager) {
if (alarmManager == null)
return;
if (isAlarmPending() && getAlarmLeadTimeMinutes() > 0) {
final PendingIntent alarmIntent = getAlarmIntent(context);
alarmManager.cancel(alarmIntent);
long alarmTime = getAlarmClockTime();
alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, alarmIntent);
if (Log.isLoggable(Constants.TAG, Log.VERBOSE))
Log.v(Constants.TAG,
"Scheduling alarm for "
+ DateFormatUtils.format(alarmTime, "h:mm:ss"));
}
}
public void cancelAlarm(Context context, AlarmManager alarmManager) {
alarmManager.cancel(getAlarmIntent(context));
this.alarmPending.setValue(false);
}
private PendingIntent notificationIntent;
private PendingIntent getNotificationIntent(Context context) {
if (notificationIntent == null) {
Intent targetIntent = new Intent(Intent.ACTION_VIEW,
getStationPair().getUri());
targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
notificationIntent = PendingIntent.getActivity(context, 0,
targetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
return notificationIntent;
}
public Notification createNotification(Context context) {
final int halfMinutes = (getMeanSecondsLeft() + 15) / 30;
float minutes = halfMinutes / 2f;
final String minutesText = (minutes < 1) ? "Less than one minute"
: (String.format("~%.1f minute", minutes) + ((minutes != 1.0) ? "s"
: ""));
final Intent cancelAlarmIntent = new Intent(context,
BoardedDepartureService.class);
cancelAlarmIntent.putExtra("cancelNotifications", true);
Builder notificationBuilder = new NotificationCompat.Builder(context)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_stat_notification)
.setContentTitle(
getOrigin().shortName + " to "
+ getPassengerDestination().shortName)
.setContentIntent(getNotificationIntent(context)).setWhen(0);
if (android.os.Build.VERSION.SDK_INT >= 16) {
notificationBuilder.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentText(minutesText + " until departure");
if (isAlarmPending()) {
notificationBuilder.addAction(
R.drawable.ic_action_cancel_alarm,
"Cancel alarm",
PendingIntent.getService(context, 0, cancelAlarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT)).setSubText(
"Alarm " + getAlarmLeadTimeMinutes()
+ " minutes before departure");
}
} else if (isAlarmPending()) {
notificationBuilder.setContentText(minutesText
+ " to departure (alarm at " + getAlarmLeadTimeMinutes()
+ " min" + ((getAlarmLeadTimeMinutes() == 1) ? "" : "s")
+ ")");
} else {
notificationBuilder
.setContentText(minutesText + " until departure");
}
return notificationBuilder.build();
}
@Override
public String toString() {
java.text.DateFormat format = SimpleDateFormat.getTimeInstance();
StringBuilder builder = new StringBuilder();
builder.append(trainDestination);
if (requiresTransfer) {
builder.append(" (w/ xfer)");
}
builder.append(", ");
builder.append(getCountdownText());
builder.append(", ");
builder.append(format.format(new Date(getMeanEstimate())));
return builder.toString();
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(origin.abbreviation);
dest.writeString(trainDestination.abbreviation);
dest.writeString(passengerDestination == null ? null
: passengerDestination.abbreviation);
dest.writeString(destinationColor);
dest.writeString(platform);
dest.writeString(direction);
dest.writeByte((byte) (bikeAllowed ? 1 : 0));
dest.writeString(trainLength);
dest.writeByte((byte) (requiresTransfer ? 1 : 0));
dest.writeInt(minutes);
dest.writeLong(minEstimate);
dest.writeLong(maxEstimate);
dest.writeLong(arrivalTimeOverride);
dest.writeInt(estimatedTripTime);
dest.writeInt(line.ordinal());
dest.writeByte(beganAsDeparted ? (byte) 1 : (byte) 0);
dest.writeByte(bikeAllowed ? (byte) 1 : (byte) 0);
dest.writeByte(requiresTransfer ? (byte) 1 : (byte) 0);
dest.writeByte(transferScheduled ? (byte) 1 : (byte) 0);
}
private void readFromParcel(Parcel in) {
origin = Station.getByAbbreviation(in.readString());
trainDestination = Station.getByAbbreviation(in.readString());
passengerDestination = Station.getByAbbreviation(in.readString());
destinationColor = in.readString();
platform = in.readString();
direction = in.readString();
bikeAllowed = in.readByte() != 0;
trainLength = in.readString();
requiresTransfer = in.readByte() != 0;
minutes = in.readInt();
minEstimate = in.readLong();
maxEstimate = in.readLong();
arrivalTimeOverride = in.readLong();
estimatedTripTime = in.readInt();
line = Line.values()[in.readInt()];
beganAsDeparted = in.readByte() == (byte) 1;
bikeAllowed = in.readByte() == (byte) 1;
requiresTransfer = in.readByte() == (byte) 1;
transferScheduled = in.readByte() == (byte) 1;
}
public static final Parcelable.Creator<Departure> CREATOR = new Parcelable.Creator<Departure>() {
public Departure createFromParcel(Parcel in) {
return new Departure(in);
}
public Departure[] newArray(int size) {
return new Departure[size];
}
};
public void notifyAlarmHasBeenHandled() {
this.alarmPending.setValue(false);
}
}