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 { 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 alarmLeadTimeMinutes = new Observable( 0); private Observable alarmPending = new Observable(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 getAlarmLeadTimeMinutesObservable() { return alarmLeadTimeMinutes; } public boolean isAlarmPending() { return alarmPending.getValue(); } public Observable 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 CREATOR = new Parcelable.Creator() { 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); } }