public interface ApiService {
    @GET("/v1/planner/")
    void getPlan(
            @Query("from") PlaceQuery from,
            @Query("to") PlaceQuery to,
            @Query("mode") String mode,
            @Query("alternative") boolean alternative,
            @Query("via") PlaceQuery via,
            @Query("arriveBy") boolean arriveBy,
            @Query("time") String time,
            @Query("travelMode") TravelModeQuery travelMode,
            @Query("dir") String dir,
            @Query("paginateRef") String paginateRef,
            Callback<Plan> callback);

    @GET("/v1/planner/intermediate/")
    void getIntermediateStops(
            @Query("reference") List<String> reference,
            Callback<IntermediateResponse> callback);

    @GET("/v1/semistatic/site/near/")
    void getNearbyStops(
            @Query("latitude") double latitude,
            @Query("longitude") double longitude,
            Callback<NearbyStopsResponse> callback);
} public class Alert implements Parcelable {
    private final String header;
    private final String description;
    private final String language;

    public Alert(String description, String header, String language) {
        this.description = description;
        this.header = header;
        this.language = language;
    }

    protected Alert(Parcel in) {
        header = in.readString();
        description = in.readString();
        language = in.readString();
    }

    public static final Creator<Alert> CREATOR = new Creator<Alert>() {
        @Override
        public Alert createFromParcel(Parcel in) {
            return new Alert(in);
        }

        @Override
        public Alert[] newArray(int size) {
            return new Alert[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(header);
        dest.writeString(description);
        dest.writeString(language);
    }

    public String getDescription() {
        return description;
    }

    public String getHeader() {
        return header;
    }

    public String getLanguage() {
        return language;
    }
} public class IntermediateResponse {
    private final List<IntermediateStops> result;

    public IntermediateResponse(List<IntermediateStops> result) {
        this.result = result;
    }

    public List<IntermediateStops> getResult() {
        return result;
    }

    public List<IntermediateStop> getStops(String reference) {
        if (result == null || result.isEmpty()) {
            return Collections.emptyList();
        }
        for (IntermediateStops is : result) {
            if (reference.equals(is.getReference())) {
                return is.getStops();
            }
        }
        return Collections.emptyList();
    }
} public class IntermediateStop extends ParcelableBase {
    private final Place location;
    private final Date startTime;
    private Date startTimeRt;
    private final Date endTime;
    private Date endTimeRt;

    public IntermediateStop(Date endTimeRt, Place location, Date startTime, Date startTimeRt, Date endTime) {
        this.endTimeRt = endTimeRt;
        this.location = location;
        this.startTime = startTime;
        this.startTimeRt = startTimeRt;
        this.endTime = endTime;
    }

    protected IntermediateStop(Parcel in) {
        location = in.readParcelable(Place.class.getClassLoader());
        startTime = readDate(in);
        startTimeRt = readDate(in);
        endTime = readDate(in);
        endTimeRt = readDate(in);
    }

    public static final Creator<IntermediateStop> CREATOR = new Creator<IntermediateStop>() {
        @Override
        public IntermediateStop createFromParcel(Parcel in) {
            return new IntermediateStop(in);
        }

        @Override
        public IntermediateStop[] newArray(int size) {
            return new IntermediateStop[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(location, flags);
        writeDate(dest, startTime);
        writeDate(dest, startTimeRt);
        writeDate(dest, endTime);
        writeDate(dest, endTimeRt);
    }

    public Place getLocation() {
        return location;
    }

    public Date getEndTime() {
        return endTime;
    }

    public Date getEndTimeRt() {
        return endTimeRt;
    }

    public Date getStartTimeRt() {
        return startTimeRt;
    }

    public Date getTime() {
        if (startTime != null) {
            return startTime;
        }
        return endTime;
    }

    public Date getTimeRt() {
        if (startTime != null) {
            return startTimeRt;
        }
        return endTimeRt;
    }

    public boolean hasDelay() {
        if (startTime != null) {
            return hasDepartureDelay();
        }
        return hasArrivalDelay();
    }

    public boolean hasDepartureDelay() {
        if (startTimeRt != null && startTime != null) {
            return true;
        }
        return false;
    }

    public boolean hasArrivalDelay() {
        if (endTimeRt != null && endTime != null) {
            return true;
        }
        return false;
    }

    public int startTimeDelay() {
        return DateTimeUtil.getDelay(startTime, startTimeRt);
    }

    public int endTimeDelay() {
        return DateTimeUtil.getDelay(endTime, endTimeRt);
    }

    public Pair<Integer, RealTimeState> delay() {
        if (startTime != null && startTimeRt != null) {
            int delay = startTimeDelay();
            return Pair.create(delay, DateTimeUtil.getRealTimeStateFromDelay(delay));
        } else if (endTime != null && endTimeRt != null) {
            int delay = endTimeDelay();
            return Pair.create(delay, DateTimeUtil.getRealTimeStateFromDelay(delay));
        }
        return Pair.create(0, RealTimeState.NOT_SET);
    }

    public void setStartTimeRt(Date startTimeRt) {
        this.startTimeRt = startTimeRt;
    }

    public void setEndTimeRt(Date endTimeRt) {
        this.endTimeRt = endTimeRt;
    }
} public class Leg extends ParcelableBase {
    private final Place from;
    private final Place to;
    private final String geometry;
    private final int distance;
    private final int duration;
    private final List<Step> steps;
    private final String travelMode;
    private final Place headsign;
    private final String routeName;
    private final String routeShortName;
    private final String routeColor;
    private final Date startTime;
    private final Date endTime;
    private Date startTimeRt;
    private Date endTimeRt;
    private int departureDelay;
    private int arrivalDelay;
    private boolean realTime;
    private final boolean cancelled;
    private final boolean unreachable;
    private final boolean invalid;
    private final boolean alternative;
    private final List<Alert> alerts;
    private final List<Alert> notes;
    private final String geometryRef;
    private final String detailRef;
    private List<IntermediateStop> intermediateStops;
    private long updatedAtMillis;

    public Leg(List<Alert> alerts, Place from, Place to, String geometry, int distance,
               int duration, List<Step> steps, String travelMode, Place headsign,
               String routeName, String routeShortName, String routeColor, Date startTime,
               Date endTime, Date startTimeRt, Date endTimeRt, int departureDelay,
               int arrivalDelay, boolean realTime, boolean cancelled, boolean unreachable,
               boolean invalid, boolean alternative, List<Alert> notes,
               String geometryRef, String detailRef) {
        this.alerts = alerts;
        this.from = from;
        this.to = to;
        this.geometry = geometry;
        this.distance = distance;
        this.duration = duration;
        this.steps = steps;
        this.travelMode = travelMode;
        this.headsign = headsign;
        this.routeName = routeName;
        this.routeShortName = routeShortName;
        this.routeColor = routeColor;
        this.startTime = startTime;
        this.endTime = endTime;
        this.startTimeRt = startTimeRt;
        this.endTimeRt = endTimeRt;
        this.departureDelay = departureDelay;
        this.arrivalDelay = arrivalDelay;
        this.realTime = realTime;
        this.cancelled = cancelled;
        this.unreachable = unreachable;
        this.invalid = invalid;
        this.alternative = alternative;
        this.notes = notes;
        this.geometryRef = geometryRef;
        this.detailRef = detailRef;
    }

    protected Leg(Parcel in) {
        from = in.readParcelable(Place.class.getClassLoader());
        to = in.readParcelable(Place.class.getClassLoader());
        geometry = in.readString();
        distance = in.readInt();
        duration = in.readInt();
        steps = in.createTypedArrayList(Step.CREATOR);
        travelMode = in.readString();
        headsign = in.readParcelable(Place.class.getClassLoader());
        routeName = in.readString();
        routeShortName = in.readString();
        routeColor = in.readString();
        startTime = readDate(in);
        endTime = readDate(in);
        startTimeRt = readDate(in);
        endTimeRt = readDate(in);
        departureDelay = in.readInt();
        arrivalDelay = in.readInt();
        realTime = in.readInt() == 1;
        cancelled = in.readInt() == 1;
        unreachable = in.readInt() == 1;
        invalid = in.readInt() == 1;
        alternative = in.readInt() == 1;
        alerts = in.createTypedArrayList(Alert.CREATOR);
        notes = in.createTypedArrayList(Alert.CREATOR);
        geometryRef = in.readString();
        detailRef = in.readString();
        intermediateStops = in.createTypedArrayList(IntermediateStop.CREATOR);
        updatedAtMillis = in.readLong();
    }

    public static final Creator<Leg> CREATOR = new Creator<Leg>() {
        @Override
        public Leg createFromParcel(Parcel in) {
            return new Leg(in);
        }

        @Override
        public Leg[] newArray(int size) {
            return new Leg[size];
        }
    };

    public int getDistance() {
        return distance;
    }

    public int getDuration() {
        return duration;
    }

    public Place getFrom() {
        return from;
    }

    public String getGeometry() {
        return geometry;
    }

    public Place getTo() {
        return to;
    }

    public List<Step> getSteps() {
        return steps;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(from, flags);
        dest.writeParcelable(to, flags);
        dest.writeString(geometry);
        dest.writeInt(distance);
        dest.writeInt(duration);
        dest.writeTypedList(steps);
        dest.writeString(travelMode);
        dest.writeParcelable(headsign, flags);
        dest.writeString(routeName);
        dest.writeString(routeShortName);
        dest.writeString(routeColor);
        dest.writeLong(startTime != null ? startTime.getTime() : -1);
        dest.writeLong(endTime != null ? endTime.getTime() : -1);
        dest.writeLong(startTimeRt != null ? startTimeRt.getTime() : -1);
        dest.writeLong(endTimeRt != null ? endTimeRt.getTime() : -1);
        dest.writeInt(departureDelay);
        dest.writeInt(arrivalDelay);
        dest.writeInt(realTime ? 1 : 0);
        dest.writeInt(cancelled ? 1 : 0);
        dest.writeInt(unreachable ? 1 : 0);
        dest.writeInt(invalid ? 1 : 0);
        dest.writeInt(alternative ? 1 : 0);
        dest.writeTypedList(alerts);
        dest.writeTypedList(notes);
        dest.writeString(geometryRef);
        dest.writeString(detailRef);
        dest.writeTypedList(intermediateStops);
        dest.writeLong(updatedAtMillis);
    }

    public Date departsAt() {
        if (startTimeRt != null) {
            return startTimeRt;
        }
        return startTime;
    }

    public Date arrivesAt() {
        if (endTimeRt != null) {
            return endTimeRt;
        }
        return endTime;
    }

    public String getTravelMode() {
        return travelMode;
    }

    public String getRouteShortName() {
        return routeShortName;
    }

    public boolean hasAlerts() {
        return alerts != null && alerts.size() > 0;
    }

    public boolean hasNotes() {
        return notes != null && notes.size() > 0;
    }

    public boolean isTransit() {
        return !"foot".equals(travelMode);
    }

    public String getRouteColor() {
        return routeColor;
    }

    public boolean isRealTime() {
        return realTime;
    }

    public Date getEndTime() {
        return endTime;
    }

    public Date getEndTimeRt() {
        return endTimeRt;
    }

    public Date getStartTime() {
        return startTime;
    }

    public Date getStartTimeRt() {
        return startTimeRt;
    }

    public String getRouteName() {
        return routeName;
    }

    public List<Alert> getAlerts() {
        return alerts;
    }

    public List<Alert> getNotes() {
        return notes;
    }

    public Place getHeadsign() {
        return headsign;
    }

    public boolean hasDepartureDelay() {
        return departureDelay != 0;
    }

    public boolean hasArrivalDelay() {
        return arrivalDelay != 0;
    }

    public boolean isOnTime(boolean isDeparture) {
        int delay = isDeparture ? departureDelay : arrivalDelay;
        return isRealTime() && delay == 0;
    }

    public boolean isLate(boolean isDeparture) {
        int delay = isDeparture ? departureDelay : arrivalDelay;
        return isRealTime() && delay > 0;
    }

    public boolean isAhead(boolean isDeparture) {
        int delay = isDeparture ? departureDelay : arrivalDelay;
        return isRealTime() && delay < 0;
    }

    public boolean hasStopIndex(int stopIndex) {
        return getFrom().getStopIndex() != stopIndex
                && getTo().getStopIndex() != stopIndex;
    }

    @NonNull
    public List<IntermediateStop> getIntermediateStops() {
        if (intermediateStops == null) {
            intermediateStops = new ArrayList<>();
        }
        return intermediateStops;
    }

    public void setIntermediateStops(List<IntermediateStop> intermediateStops) {
        this.intermediateStops = intermediateStops;
    }

    public String getDetailRef() {
        return detailRef;
    }

    public RealTimeState realtimeState(boolean isDeparture) {
        if (isOnTime(isDeparture)) {
            return RealTimeState.ON_TIME;
        } else if (isAhead(isDeparture)) {
            return RealTimeState.AHEAD_OF_SCHEDULE;
        } else if (isLate(isDeparture)) {
            return RealTimeState.BEHIND_SCHEDULE;
        }
        return RealTimeState.NOT_SET;
    }

    public boolean updateTimes(List<IntermediateStop> stopTimes) {
        boolean updated = false;

        // If intermediate stops is missing, attempt to add them.
        if (getIntermediateStops().isEmpty()) {
            boolean addStopTime = false;
            for (IntermediateStop stopTime : stopTimes) {
                // Add all stop times that is between start and end.
                if (stopTime.getLocation().looksEquals(getTo())) {
                    addStopTime = false;
                }
                if (addStopTime) {
                    intermediateStops.add(stopTime);
                }
                if (stopTime.getLocation().looksEquals(getFrom())) {
                    addStopTime = true;
                }
            }
        }

        for (IntermediateStop stopTime : stopTimes) {
            // Check from if we match update start time
            if (stopTime.getLocation().looksEquals(getFrom())) {
                startTimeRt = stopTime.getStartTimeRt();
                if (stopTime.hasDepartureDelay()) {
                    realTime = true;
                    departureDelay = stopTime.startTimeDelay();
                } else {
                    realTime = false;
                    departureDelay = 0;
                }
                updated = true;
            } else if (stopTime.getLocation().looksEquals(getTo())) {
                endTimeRt = stopTime.getEndTimeRt();
                if (stopTime.hasArrivalDelay()) {
                    realTime = true;
                    arrivalDelay = stopTime.endTimeDelay();
                } else {
                    realTime = false;
                    arrivalDelay = 0;
                }
                updated = true;
            }

            if (intermediateStops != null && !intermediateStops.isEmpty()) {
                for (IntermediateStop intermediateStop : intermediateStops) {
                    if (stopTime.getLocation().looksEquals(intermediateStop.getLocation())) {
                        intermediateStop.setStartTimeRt(stopTime.getStartTimeRt());
                        intermediateStop.setEndTimeRt(stopTime.getEndTimeRt());
                        updated = true;
                    }
                }
            }
        }
        if (updated) {
            updatedAtMillis = System.currentTimeMillis();
        }
        return updated;
    }

    /**
     * Checks if this leg is considered to be active.
     *
     * @param timeMillis
     * @return
     */
    public boolean shouldRefresh(long timeMillis) {
        // Do we have real-time?
        if (isRealTime()) {
            return true;
        }
        // 1 hour before departure, 30 minutes after arrival.
        if (departsAt().getTime() - 3600000 > timeMillis
                && arrivesAt().getTime() + 1800000 < timeMillis) {
            return true;
        }
        // If we haven't been refreshed in an hour.
        if (updatedAtMillis != 0 && updatedAtMillis < timeMillis - 3600000) {
            return true;
        }
        return false;
    }
} public class Place implements Parcelable {
    private final String id;
    private final String name;
    private final String type;
    private final double lat;
    private final double lon;
    private final int stopIndex;
    @Nullable
    private final String track;
    @Nullable
    private final List<Entrance> entrances;

    public Place(String id, String name, String type, double lat, double lon,
                 int stopIndex, @Nullable String track, @Nullable List<Entrance> entrances) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.lat = lat;
        this.lon = lon;
        this.stopIndex = stopIndex;
        this.track = track;
        this.entrances = entrances;
    }

    protected Place(Parcel in) {
        id = in.readString();
        name = in.readString();
        type = in.readString();
        lat = in.readDouble();
        lon = in.readDouble();
        stopIndex = in.readInt();
        track = in.readString();
        entrances = in.createTypedArrayList(Entrance.CREATOR);
    }

    public static final Creator<Place> CREATOR = new Creator<Place>() {
        @Override
        public Place createFromParcel(Parcel in) {
            return new Place(in);
        }

        @Override
        public Place[] newArray(int size) {
            return new Place[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeString(name);
        dest.writeString(type);
        dest.writeDouble(lat);
        dest.writeDouble(lon);
        dest.writeInt(stopIndex);
        dest.writeString(track);
        dest.writeTypedList(entrances);
    }

    public String getId() {
        return id;
    }

    public double getLat() {
        return lat;
    }

    public double getLon() {
        return lon;
    }

    public String getName() {
        return name;
    }

    public String getType() {
        return type;
    }

    public boolean isMyLocation() {
        return name != null && name.equals(Site.TYPE_MY_LOCATION);
    }

    public boolean hasLocation() {
        // Sorry Null Island.
        return lat != 0 && lon != 0;
    }

    public int getStopIndex() {
        return stopIndex;
    }

    public boolean hasEntrances() {
        return entrances != null && !entrances.isEmpty();
    }

    public List<Entrance> getEntrances() {
        return entrances;
    }

    @Nullable
    public String getTrack() {
        return track;
    }

    public boolean looksEquals(Place other) {
        if (id != null && other.getId() != null) {
            return id.equals(other.getId());
        } else if (stopIndex == other.getStopIndex()) {
            return true;
        } else if (name != null && other.getName() != null) {
            return name.equals(other.getName());
        }
        return false;
    }
} public enum RealTimeState {
    BEHIND_SCHEDULE,
    AHEAD_OF_SCHEDULE,
    ON_TIME,
    NOT_SET,
} public class Route implements Parcelable {
    private final int duration;
    private final List<Leg> legs;
    private final String mode;
    private final Fare fare;

    public Route(int duration, List<Leg> legs, String mode, Fare fare) {
        this.duration = duration;
        this.legs = legs;
        this.mode = mode;
        this.fare = fare;
    }

    protected Route(Parcel in) {
        duration = in.readInt();
        legs = new ArrayList<>();
        in.readTypedList(legs, Leg.CREATOR);
        mode = in.readString();
        fare = in.readParcelable(Fare.class.getClassLoader());
    }

    public static final Creator<Route> CREATOR = new Creator<Route>() {
        @Override
        public Route createFromParcel(Parcel in) {
            return new Route(in);
        }

        @Override
        public Route[] newArray(int size) {
            return new Route[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(duration);
        dest.writeTypedList(legs);
        dest.writeString(mode);
        dest.writeParcelable(fare, flags);
    }

    public int getDuration() {
        return duration;
    }

    public List<Leg> getLegs() {
        return legs;
    }

    public String getMode() {
        return mode;
    }

    public Pair<Date, RealTimeState> departsAt(boolean useTransitTime) {
        if (useTransitTime) {
            for (Leg leg : legs) {
                if (leg.isTransit()) {
                    return Pair.create(leg.departsAt(), leg.realtimeState(true));
                }
            }
        }
        // If no transit leg, get the first.
        Leg leg = legs.get(0);
        return Pair.create(leg.departsAt(), leg.realtimeState(true));
    }

    public Pair<Date, RealTimeState> arrivesAt(boolean useTransitTime) {
        if (useTransitTime) {
            for (int i = legs.size() - 1; i >= 0; i--) {
                Leg leg = legs.get(i);
                if (leg.isTransit()) {
                    return Pair.create(leg.arrivesAt(), leg.realtimeState(false));
                }
            }
        }
        Leg leg = legs.get(legs.size() - 1);
        return Pair.create(leg.arrivesAt(), leg.realtimeState(false));
    }

    public Place fromStop() {
        for (Leg leg : legs) {
            if (leg.isTransit()) {
                return leg.getFrom();
            }
        }
        // If no transit legs, get the first.
        return legs.get(0).getFrom();
    }

    public Place toStop() {
        for (int i = legs.size() - 1; i >= 0; i--) {
            Leg leg = legs.get(i);
            if (leg.isTransit()) {
                return leg.getTo();
            }
        }
        // If no transit leg, get the last.
        return legs.get(legs.size() - 1).getTo();
    }

    public boolean hasAlertsOrNotes() {
        for (Leg leg : legs) {
            if (leg.hasAlerts() || leg.hasNotes()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return returns true if this trip can be purchased with SMS.
     */
    public boolean canBuyTicket() {
        if (fare == null) {
            return false;
        }
        return fare.canBuyTicket();
    }

    public Fare getFare() {
        return fare;
    }

} public class TravelMode {
    public final static String FOOT = "foot";
    public final static String BIKE = "bike";
    public final static String BIKE_RENTAL = "bikeRent";
    public final static String CAR = "car";
    public final static String METRO = "metro";
    public final static String BUS = "bus";
    public final static String TRAIN = "train";
    public final static String LIGHT_TRAIN = "lightTrain";
    public final static String TRAM = "tram";
    public final static String BOAT = "boat";

    private final String mode;

    public TravelMode(String mode) {
        this.mode = mode;
    }

    public String getMode() {
        return mode;
    }

    @Override
    public String toString() {
        return mode;
    }
} public static final class Journeys implements BaseColumns {
    public static final Uri CONTENT_URI =
        Uri.parse("content://" + JourneysProvider.AUTHORITY + "/journeys");
    public static final String CONTENT_TYPE =
        "vnd.android.cursor.dir/vnd.sthlmtraveling.journeys";
            
    public static final int JOURNEY_ID_PATH_POSITION = 1;

    /**
     * The default number of journeys to be saved in history.
     * </p>
     * Total number of journeys is history size + starred journeys.
     */
    public static final int DEFAULT_HISTORY_SIZE = 5;

    public static final String ID = "_id";

    /**
     * The name, example "Take me home" or "To work".
     */
    public static final String NAME = "name";

    /**
     * The position in a list.
     */
    public static final String POSITION = "position";

    /**
     * If the journey is starred.
     */
    public static final String STARRED = "starred";

    /**
     * When the entry was created.
     */
    public static final String CREATED_AT = "created_at";

    /**
     * When the entry was updated.
     */
    public static final String UPDATED_AT = "updated_at";

    /**
     * The journey data as json.
     */
    public static final String JOURNEY_DATA = "journey_data";

    /**
     * The default sort order.
     */
    public static final String DEFAULT_SORT_ORDER =
        "position DESC, updated_at DESC, created_at DESC";

    /**
     * The history sort order, this also includes a limit.
     */
    public static final String HISTORY_SORT_ORDER =
        "updated_at DESC, created_at DESC LIMIT " + DEFAULT_HISTORY_SIZE;


    private Journeys() {
    }
} public class JourneyQuery implements Parcelable {
    public Site origin;
    public Site destination;
    public Site via;
    public Date time;
    public boolean isTimeDeparture = true;
    public boolean alternativeStops = false;
    public List<String> transportModes = new ArrayList<>();
    public String ident;
    public boolean hasPromotions;
    public int promotionNetwork = -1;
    // Storing the state of the current ident and scroll dir to allow refresh of paginated
    // results
    @Nullable  public String previousIdent;
    @Nullable  public String previousDir;

    public JourneyQuery() {
    }

    public JourneyQuery(Parcel parcel) {
        origin = parcel.readParcelable(Site.class.getClassLoader());
        destination = parcel.readParcelable(Site.class.getClassLoader());
        via = parcel.readParcelable(Site.class.getClassLoader());
        time = new Date(parcel.readLong());
        isTimeDeparture = (parcel.readInt() == 1);
        alternativeStops = (parcel.readInt() == 1);
        transportModes = new ArrayList<String>();
        parcel.readStringList(transportModes);
        ident = parcel.readString();
        hasPromotions = (parcel.readInt() == 1);
        promotionNetwork = parcel.readInt();
        previousIdent = parcel.readString();
        previousDir = parcel.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(origin, 0);
        dest.writeParcelable(destination, 0);
        dest.writeParcelable(via, 0);
        dest.writeLong(time.getTime());
        dest.writeInt(isTimeDeparture ? 1 : 0);
        dest.writeInt(alternativeStops ? 1 : 0);
        dest.writeStringList(transportModes);
        dest.writeString(ident);
        dest.writeInt(hasPromotions ? 1 : 0);
        dest.writeInt(promotionNetwork);
        dest.writeString(previousIdent);
        dest.writeString(previousDir);
    }

    /**
     * Checks if the query contains has via.
     * 
     * @return Returns <code>true</code> if a via location is set,
     * <code>false</code> otherwise.
     */
    public boolean hasVia() {
        return via != null && via.hasName();
    }

    /**
     * Returns true if anything has than the defaults has been modified.
     *
     * @return true if any filtering is active.
     */
    public boolean hasAdditionalFiltering() {
        if (hasVia()) {
            return true;
        }
        if (alternativeStops) {
            return true;
        }
        if (transportModes != null) {
            return !hasDefaultTransportModes();
        }
        return false;
    }

    public boolean hasDefaultTransportModes() {
        if (transportModes != null) {
            List<String> defaults = Arrays.asList(TransportMode.METRO, TransportMode.BUS,
                    TransportMode.WAX, TransportMode.TRAIN, TransportMode.TRAM);
            if (transportModes.containsAll(defaults)) {
                return true;
            }
        }
        return false;
    }


    public static final Creator<JourneyQuery> CREATOR = new Creator<JourneyQuery>() {
        public JourneyQuery createFromParcel(Parcel parcel) {
            return new JourneyQuery(parcel);
        }

        public JourneyQuery[] newArray(int size) {
            return new JourneyQuery[size];
        }
    };

    public static JSONObject siteToJson(Site site) throws JSONException {
        JSONObject json = new JSONObject();

        json.put("id", site.getId());
        json.put("name", site.getName());
        if (site.isMyLocation()) {
            json.put("latitude", 0);
            json.put("longitude", 0);
        } else if (site.hasLocation()) {
            json.put("latitude", (int) (site.getLocation().getLatitude() * 1E6));
            json.put("longitude", (int) (site.getLocation().getLongitude() * 1E6));
        }
        json.put("source", site.getSource());
        json.put("locality", site.getLocality());

        return json;
    }

    public static Site jsonToSite(JSONObject json) throws JSONException {
        Site site = new Site();

        if (json.has("id")) {
            site.setId(json.getString("id"));
        }
        if (json.has("locality")) {
            site.setLocality(json.getString("locality"));
        }
        if (json.has("source")) {
            site.setSource(json.getInt("source"));
        }
        if (json.has("latitude") && json.has("longitude")) {
            site.setLocation(json.getInt("latitude"), json.getInt("longitude"));
        }

        site.setName(json.getString("name"));

        return site;
    }


    public JSONObject toJson(boolean all) throws JSONException {
        JSONObject jsonOrigin = siteToJson(origin);
        JSONObject jsonDestination = siteToJson(destination);

        JSONObject jsonQuery = new JSONObject();
        if (via != null) {
            JSONObject jsonVia = siteToJson(via);
            jsonQuery.put("via", jsonVia);
        }

        if (transportModes != null) {
            jsonQuery.put("transportModes", new JSONArray(transportModes));
        }

        jsonQuery.put("alternativeStops", alternativeStops);
        jsonQuery.put("origin", jsonOrigin);
        jsonQuery.put("destination", jsonDestination);

//        if (all) {
//            jsonQuery.put("ident", ident);
//            jsonQuery.put("time", time.format("%F %R"));
//            jsonQuery.put("isTimeDeparture", this.isTimeDeparture);
//        }

        return jsonQuery;
    }

    public static JourneyQuery fromJson(JSONObject jsonObject)
            throws JSONException {
        JourneyQuery journeyQuery = new JourneyQuery.Builder()
            .origin(jsonObject.getJSONObject("origin"))
            .destination(jsonObject.getJSONObject("destination"))
            .transportModes(jsonObject.has("transportModes")
                    ? jsonObject.getJSONArray("transportModes") : null)
            .create();

        if (jsonObject.has("isTimeDeparture")) {
            journeyQuery.isTimeDeparture =
                jsonObject.getBoolean("isTimeDeparture");
        }
        if (jsonObject.has("alternativeStops")) {
            journeyQuery.alternativeStops =
                jsonObject.getBoolean("alternativeStops");
        }
        if (jsonObject.has("via")) {
            JSONObject jsonVia = jsonObject.getJSONObject("via");
            journeyQuery.via = jsonToSite(jsonVia);
        }

        return journeyQuery;
    }

    public Uri toUri(boolean withTime) {
        if (origin == null || destination == null) {
            return null;
        }

        Uri routesUri;

        PlaceQuery fromQuery = new PlaceQuery.Builder().place(origin.asPlace()).build();
        PlaceQuery toQuery = new PlaceQuery.Builder().place(destination.asPlace()).build();

        routesUri = Uri.parse("journeyplanner://routes?");

        routesUri = routesUri.buildUpon()
                .appendQueryParameter("version", "2")
                .appendQueryParameter("from", Uri.encode(fromQuery.toString()))
                .appendQueryParameter("fromSource", String.valueOf(origin.getSource()))
                .appendQueryParameter("to", Uri.encode(toQuery.toString()))
                .appendQueryParameter("toSource", String.valueOf(destination.getSource()))
                .appendQueryParameter("alternative", String.valueOf(alternativeStops))
                .build();

        if (withTime) {
            if (time != null) {
                String timeString = DateTimeUtil.format2445(time);
                routesUri = routesUri.buildUpon()
                        .appendQueryParameter("time", timeString)
                        .appendQueryParameter("isTimeDeparture", String.valueOf(isTimeDeparture))
                        .build();
            }
        }

        if (hasVia()) {
            routesUri = routesUri.buildUpon()
                    .appendQueryParameter("via", Uri.encode(
                            new PlaceQuery.Builder().place(via.asPlace()).build().toString()))
                    .build();
        }

        if (transportModes != null && !transportModes.isEmpty()) {
            // Convert transport modes to travel modes.
            List<TravelMode> travelModes = LegUtil.transportModesToTravelModes(transportModes);
            TravelModeQuery travelModeQuery = new TravelModeQuery(travelModes);
            routesUri = routesUri.buildUpon()
                    .appendQueryParameter("travelMode", travelModeQuery.toString())
                    .build();
        }

        return routesUri;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "JourneyQuery [alternativeStops=" + alternativeStops
                + ", destination=" + destination + ", ident=" + ident
                + ", isTimeDeparture=" + isTimeDeparture + ", origin=" + origin
                + ", time=" + time + ", transportModes="
                + transportModes + ", via=" + via + "]";
    }

    public static class Builder {
        private Site mOrigin;
        private Site mDestination;
        private Site mVia;
        private Date mTime;
        private boolean mIsTimeDeparture = true;
        private boolean mAlternativeStops = false;
        private List<String> mTransportModes;

        public Builder() {
            
        }

        public Builder origin(Site origin) {
            mOrigin = origin;
            return this;
        }

        public Builder origin(String name, int latitude, int longitude) {
            mOrigin = new Site();
            mOrigin.setName(name);
            mOrigin.setLocation(latitude, longitude);
            return this;
        }

        public Builder origin(JSONObject jsonObject) throws JSONException {
            mOrigin = jsonToSite(jsonObject);
            return this;
        }
        
        public Builder destination(Site destination) {
            mDestination = destination;
            return this;
        }

        public Builder destination(JSONObject jsonObject) throws JSONException {
            mDestination = jsonToSite(jsonObject);
            return this;
        }
        
        public Builder destination(String name, int latitude, int longitude) {
            mDestination = new Site();
            mDestination.setName(name);
            mDestination.setLocation(latitude, longitude);
            return this;
        }

        public Builder via(Site via) {
            if (via != null && via.hasName()) {
                mVia = via;
            }
            return this;
        }

        public Builder via(JSONObject jsonObject) throws JSONException {
            mVia = jsonToSite(jsonObject);
            return this;
        }

        public Builder time(Date time) {
            mTime = time;
            return this;
        }

        public Builder isTimeDeparture(boolean isTimeDeparture) {
            mIsTimeDeparture = isTimeDeparture;
            return this;
        }

        public Builder alternativeStops(boolean alternativeStops) {
            mAlternativeStops = alternativeStops;
            return this;
        }

        public Builder transportModes(List<String> transportModes) {
            mTransportModes = transportModes;
            return this;
        }

        public Builder transportModes(JSONArray jsonArray) throws JSONException {
            if (jsonArray == null) {
                return this;
            }
            mTransportModes = new ArrayList<String>();
            for (int i = 0; i < jsonArray.length(); i++) {
                mTransportModes.add(jsonArray.getString(i));
            }
            return this;
        }

        public Builder uri(Uri uri) {
            String version = uri.getQueryParameter("version");
            if (version == null) {
                return uriV1(uri);
            }
            return uriV2(uri);
        }

        public Builder uriV2(Uri uri) {
            Site origin = fromQueryParameter(Uri.decode(uri.getQueryParameter("from")));
            origin.setSource(Integer.parseInt(uri.getQueryParameter("fromSource")));
            origin(origin);

            Site destination = fromQueryParameter(Uri.decode(uri.getQueryParameter("to")));
            destination.setSource(Integer.parseInt(uri.getQueryParameter("fromSource")));
            destination(destination);

            via(fromQueryParameter(Uri.decode(uri.getQueryParameter("via"))));
            alternativeStops(Boolean.parseBoolean(uri.getQueryParameter("alternative")));

            String timeStr = uri.getQueryParameter("time");
            if (timeStr != null) {
                time(DateTimeUtil.parse2445(timeStr));
                isTimeDeparture(Boolean.parseBoolean(uri.getQueryParameter("isTimeDeparture")));
            } else {
                isTimeDeparture(true);
            }

            TravelModeQuery travelModeQuery = TravelModeQuery.fromStringList(
                    Uri.decode(uri.getQueryParameter("travelMode")));
            transportModes(LegUtil.travelModesToTransportModes(travelModeQuery.getModes()));

            return this;
        }

        Site fromQueryParameter(String parameter) {
            if (parameter == null) {
                return null;
            }
            Site site = new Site();
            PlaceQuery fromQuery = new PlaceQuery.Builder().param(parameter).build();
            site.setName(fromQuery.getName());

            if (fromQuery.getId() != null) {
                site.setId(fromQuery.getId());
            }
            if (fromQuery.getLat() != 0 && fromQuery.getLon() != 0) {
                site.setLocation(fromQuery.getLat(), fromQuery.getLon());
            }

            fromQuery = null;
            return site;
        }

        public Builder uriV1(Uri uri) {
            Site origin = new Site();
            origin.setName(uri.getQueryParameter("start_point"));
            String originId = uri.getQueryParameter("start_point_id");
            if (!TextUtils.isEmpty(originId) && !"null".equals(originId)) {
                origin.setId(originId);
            }
            if (!TextUtils.isEmpty(uri.getQueryParameter("start_point_lat"))
                    && !TextUtils.isEmpty(uri.getQueryParameter("start_point_lng"))) {
                origin.setLocation(
                        (int) (Double.parseDouble(uri.getQueryParameter("start_point_lat")) * 1E6),
                        (int) (Double.parseDouble(uri.getQueryParameter("start_point_lng")) * 1E6));
            }
            if (!TextUtils.isEmpty(uri.getQueryParameter("start_point_source"))) {
                origin.setSource(Integer.parseInt(uri.getQueryParameter("start_point_source")));
            }
            this.origin(origin);

            Site destination = new Site();
            destination.setName(uri.getQueryParameter("end_point"));
            String destinationId = uri.getQueryParameter("end_point_id");
            if (!TextUtils.isEmpty(destinationId) && !"null".equals(destinationId)) {
                destination.setId(destinationId);
            }
            if (!TextUtils.isEmpty(uri.getQueryParameter("end_point_lat"))
                    && !TextUtils.isEmpty(uri.getQueryParameter("end_point_lng"))) {
                destination.setLocation(
                        (int) (Double.parseDouble(uri.getQueryParameter("end_point_lat")) * 1E6),
                        (int) (Double.parseDouble(uri.getQueryParameter("end_point_lng")) * 1E6));
            }
            if (!TextUtils.isEmpty(uri.getQueryParameter("end_point_source"))) {
                destination.setSource(Integer.parseInt(uri.getQueryParameter("end_point_source")));
            }
            this.destination(destination);

            boolean isTimeDeparture = true;
            if (!TextUtils.isEmpty(uri.getQueryParameter("isTimeDeparture"))) {
                isTimeDeparture = Boolean.parseBoolean(
                        uri.getQueryParameter("isTimeDeparture"));
            }
            this.isTimeDeparture(isTimeDeparture);


            Date time = new Date();
            String timeString = uri.getQueryParameter("time");
            if (!TextUtils.isEmpty(timeString)) {
                // TODO: What is the format here?
                //jq.time.parse(timeString);
            } else {
                time.setTime(System.currentTimeMillis());
            }
            this.time(time);

            return this;
        }

        public JourneyQuery create() {
            JourneyQuery journeyQuery = new JourneyQuery();
            journeyQuery.origin = mOrigin;
            journeyQuery.destination = mDestination;
            journeyQuery.via = mVia;

            if (mTime == null) {
                mTime = new Date();
            }
            journeyQuery.time = mTime;
            journeyQuery.isTimeDeparture = mIsTimeDeparture;
            journeyQuery.alternativeStops = mAlternativeStops;
            if (mTransportModes == null || mTransportModes.isEmpty()) {
                mTransportModes = Arrays.asList(TransportMode.METRO, TransportMode.BUS,
                        TransportMode.WAX, TransportMode.TRAIN, TransportMode.TRAM);
            }
            journeyQuery.transportModes = mTransportModes;

            return journeyQuery;
        }

    }

} public class Site implements Parcelable {
    public static final String TYPE_MY_LOCATION = "MY_LOCATION";

    public static final String TYPE_TRANSIT_STOP = "S";
    public static final String TYPE_ADDRESS = "A";

    public static final int CATEGORY_UNKNOWN = 0;
    public static final int CATEGORY_TRANSIT_STOP = 1;
    public static final int CATEGORY_ADDRESS = 2;

    public static int SOURCE_STHLM_TRAVELING = 0;
    public static int SOURCE_GOOGLE_PLACES = 1;
    private static String NAME_RE = "[^\\p{Alnum}\\(\\)\\s]";

    private String mId;
    private String mName;
    private String mLocality;
    private String mType;
    private Location mLocation;
    private int mSource = SOURCE_STHLM_TRAVELING;

    public Site() {
    }

    /**
     * Create a new Stop that is a copy of the given Stop.
     * @param site the site
     */
    public Site(Site site) {
        mName = site.getName();
        mLocality = site.getLocality();
        mLocation = site.getLocation();
        mId = site.getId();
        mType = site.getType();
        mSource = site.getSource();
    }

    public String getType() {
        return mType;
    }

    public Site(Parcel parcel) {
        mId = parcel.readString();
        mName = parcel.readString();
        mType = parcel.readString();
        double latitude = parcel.readDouble();
        double longitude = parcel.readDouble();
        if (latitude > 0 && longitude > 0) {
            Location location = new Location("sthlmtraveling");
            location.setLatitude(latitude);
            location.setLongitude(longitude);
            setLocation(location);
        }
        mLocality = parcel.readString();
        mSource = parcel.readInt();
    }

    /**
     * @return the id
     */
    public String getId() {
        return mId;
    }

    /**
     * @return the name
     */
    public String getName() {
        return mName;
    }

    /**
     * @param id the id to set
     */
    public void setId(int id) {
        if (id == 0) {
            mId = null;
        } else {
            mId = String.valueOf(id);
        }
    }

    /**
     * @param id the id to set
     */
    public void setId(String id) {
        mId = id;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        if (!TextUtils.isEmpty(name)) {
            if (name.equals(TYPE_MY_LOCATION)) {
                mName = name;
            } else {
                //mName = name.trim().replaceAll(NAME_RE, "");
                mName = name;
            }
        }
    }

    public void setLocality(String locality) {
        mLocality = locality;
    }

    public String getLocality() {
        return mLocality;
    }

    public int getSource() {
        return mSource;
    }

    public void setSource(int source) {
        this.mSource = source;
    }

    public void setType(String type) {
        mType = type;
    }

    public void setLocation(Location location) {
        mLocation = location;
    }

    public Location getLocation() {
        return mLocation;
    }

    public boolean isAddress() {
        return mType != null && mType.equals("A");
    }

    public boolean isTransitStop() {
        return mType != null && mType.equals("S");
    }

    public boolean hasType() {
        return !TextUtils.isEmpty(mType);
    }

    public boolean hasName() {
        return !TextUtils.isEmpty(mName);
    }

    @Override
    public String toString() {
        return mName;  // This is used by adapters.
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(mId);
        parcel.writeString(mName);
        parcel.writeString(mType);
        if (this.hasLocation()) {
            parcel.writeDouble(mLocation.getLatitude());
            parcel.writeDouble(mLocation.getLongitude());
        } else {
            parcel.writeDouble(0);
            parcel.writeDouble(0);
        }
        parcel.writeString(mLocality);
        parcel.writeInt(mSource);
    }

    public static final Parcelable.Creator<Site> CREATOR = new Parcelable.Creator<Site>() {
        public Site createFromParcel(Parcel in) {
            return new Site(in);
        }

        public Site[] newArray(int size) {
            return new Site[size];
        }
    };

//    public static Site fromPlannerLocation(Planner.Location loc) {
//        Site s = new Site();
//        s.setId(String.valueOf(loc.id));
//        s.setLocation(loc.latitude, loc.longitude);
//        s.setName(loc.name);
//        return s;
//    }

    public static Site fromJson(JSONObject json) throws JSONException {
        Site site = new Site();
        site.setSource(Site.SOURCE_STHLM_TRAVELING);
        site.setId(String.valueOf(json.getInt("site_id")));

        Pair<String, String> nameAndLocality =
                SitesStore.nameAsNameAndLocality(json.getString("name"));
        site.setName(nameAndLocality.first);
        site.setLocality(nameAndLocality.second);
        if (json.has("type")) {
            site.setType(json.getString("type"));
        }
        if (json.has("location") && !json.isNull("location")) {
            JSONObject locationJson = json.getJSONObject("location");
            try {
                Location location = new Location("sthlmtraveling");
                location.setLatitude(locationJson.getDouble("latitude"));
                location.setLongitude(locationJson.getDouble("longitude"));
                site.setLocation(location);
            } catch(Exception e) {
                Log.e("SITE", e.getMessage());
            }
        }
        return site;
    }

    public boolean isMyLocation() {
        return hasName() && mName.equals(TYPE_MY_LOCATION);
    }

    public boolean looksValid() {
        if (isMyLocation()) {
            return true;
        }
        if (hasLocation() && hasName() && mId == null) {
            return true;
        }
        if (hasName() && mId != null) {
            return true;
        }
        return false;
    }

    public static boolean looksValid(String name) {
        if (TextUtils.isEmpty(name) || TextUtils.getTrimmedLength(name) == 0) {
            return false;
        }
        return !name.matches(NAME_RE);
    }

    /**
     * Fill this Site with the values from another Site. If other is null this will be nullified.
     * @param value
     */
    public void fromSite(Site value) {
        if (value != null) {
            mId = value.mId;
            mLocation = value.mLocation;
            mName = value.mName;
            mType = value.mType;
            mLocality = value.mLocality;
        } else {
            mId = null;
            mLocation = null;
            mName = null;
            mType = null;
            mLocality = null;
        }
    }

    public String getNameOrId() {
        if (hasLocation() || mId == null) {
            return mName;
        }
        return String.valueOf(mId);
    }

    public boolean hasLocation() {
        return mLocation != null;
    }

    public void setLocation(double lat, double lng) {
        mLocation = new Location("sthlmtraveling");
        mLocation.setLatitude(lat);
        mLocation.setLongitude(lng);
    }

    public void setLocation(int lat, int lng) {
        if (lat == 0 || lng == 0) {
            return;
        }
        mLocation = new Location("sthlmtraveling");
        mLocation.setLatitude(lat / 1E6);
        mLocation.setLongitude(lng / 1E6);
    }

    public String toDump() {
        return "Site [mId=" + mId
                + ", mName=" + mName
                + ", mType=" + mType
                + ", mLocation=" + mLocation
                + ", mSource=" + mSource
                + ", mLocality=" + mLocality
                + "]";
    }

    public Place asPlace() {
        // If type is sthlm traveling and id is not 0.
        String id = mSource == SOURCE_STHLM_TRAVELING && !"0".equals(mId) ? mId : null;

        double lat = 0;
        double lon = 0;
        if (hasLocation()) {
            lat = mLocation.getLatitude();
            lon = mLocation.getLongitude();
        }
        return new Place(
                id,
                mName,
                isTransitStop() ? "stop" : "place",
                lat, lon, -1, null, null);
    }

    public static Site toSite(Place place) {
        Site site = new Site();
        site.setSource(SOURCE_STHLM_TRAVELING);
        site.setId(place.getId());
        site.setName(place.getName());
        site.setType("stop".equals(place.getType()) ? TYPE_TRANSIT_STOP : TYPE_ADDRESS);
        if (place.hasLocation()) {
            site.setLocation(place.getLat(), place.getLon());
        }
        return site;
    }
} public class LegViewModel implements Parcelable {
    public Leg leg;
    public boolean isExpanded;

    public LegViewModel(Leg leg) {
        this.leg = leg;
    }

    protected LegViewModel(Parcel in) {
        leg = in.readParcelable(Leg.class.getClassLoader());
        isExpanded = in.readByte() != 0;
    }

    public static final Creator<LegViewModel> CREATOR = new Creator<LegViewModel>() {
        @Override
        public LegViewModel createFromParcel(Parcel in) {
            return new LegViewModel(in);
        }

        @Override
        public LegViewModel[] newArray(int size) {
            return new LegViewModel[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(leg, flags);
        dest.writeByte((byte) (isExpanded ? 1 : 0));
    }
} public class TicketDialogFragment extends BottomSheetDialogFragment {

    static final String ARG_FARE = "zones";

    public static TicketDialogFragment create(Fare fare) {
        Bundle b = new Bundle();
        b.putParcelable(ARG_FARE, fare);
        TicketDialogFragment fragment = new TicketDialogFragment();
        fragment.setArguments(b);
        return fragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.dialog_ticket, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Bundle args = getArguments();
        Fare fare = args.getParcelable(ARG_FARE);

        RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

        final TicketAdapter adapter = new TicketAdapter(getContext());
        for (FareAttribute fareAttribute : fare.getAttributes()) {
            CharSequence description = getText(fareAttribute.isReduced() ?
                    R.string.sms_ticket_price_reduced :
                    R.string.sms_ticket_price_full);
            adapter.addTicket(new TicketPrice(description,
                    fare.getZones(),
                    fareAttribute.getText(),
                    fareAttribute.isReduced(),
                    fareAttribute.getAction()));
        }

        recyclerView.setAdapter(adapter);
        ItemClickSupport.addTo(recyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
            @Override
            public void onItemClicked(RecyclerView recyclerView, int position, View v) {
                TicketItem item = adapter.getItem(position);
                switch (item.viewType) {
                    case TicketItem.PRICE_ITEM:
                        TicketPrice ticketPrice = (TicketPrice) item.object;
                        Uri uri = Uri.parse(ticketPrice.getAction());
                        String action = uri.getQueryParameter("action");
                        if ("sms".equals(action)) {
                            sendSms(getContext(), ticketPrice.isReduced,
                                    uri.getQueryParameter("number"),
                                    uri.getQueryParameter("message"));
                        }
                        break;
                    case TicketItem.HEADER_ITEM:
                        // No-op
                        break;
                }
            }
        });
    }

    /**
     * Invokes the Messaging application.
     *
     * @param reducedPrice True if the price is reduced, false otherwise.
     * @param number       The number
     * @param message      The message
     */
    private static void sendSms(final Context context, final boolean reducedPrice,
                                final String number, final String message) {
        String price = reducedPrice ? "R" : "H"; // Only kept for analytic purposes
        Analytics.getInstance(context).event("Ticket", "Buy SMS Ticket", price);
        IntentUtil.smsIntent(context, number, message);
    }

    static class TicketPrice {
        public String zones;
        public CharSequence description;
        public boolean isReduced;
        public String priceText;
        public String action;

        public TicketPrice(CharSequence description,
                           String zones,
                           String priceText,
                           boolean isReduced,
                           String action) {
            this.description = description;
            this.zones = zones;
            this.priceText = priceText;
            this.isReduced = isReduced;
            this.action = action;
        }

        public CharSequence getPrice() {
            return priceText;
        }

        public String getAction() {
            return action;
        }
    }

    static class TicketAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        private final List<TicketItem> mTicketItems = new ArrayList<>();
        private final LayoutInflater mLayoutInflater;
        private final Context mContext;

        public TicketAdapter(Context context) {
            this.mContext = context;
            this.mLayoutInflater = LayoutInflater.from(context);
        }

        @Override
        public int getItemViewType(int position) {
            return mTicketItems.get(position).viewType;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            switch (viewType) {
                case TicketItem.PRICE_ITEM:
                    return PriceViewHolder.create(mLayoutInflater, parent);
            }
            return null;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            switch (getItemViewType(position)) {
                case TicketItem.PRICE_ITEM:
                    PriceViewHolder.bindTo((PriceViewHolder) holder,
                            (TicketPrice) mTicketItems.get(position).object);
                    break;
            }
        }

        @Override
        public int getItemCount() {
            return mTicketItems.size();
        }

        public void addTicket(TicketPrice ticket) {
            mTicketItems.add(new TicketItem<>(ticket, TicketItem.PRICE_ITEM));
        }

        public TicketItem getItem(int position) {
            return mTicketItems.get(position);
        }
    }

    public static class TicketItem<T> {

        @Retention(RetentionPolicy.SOURCE)
        @IntDef({HEADER_ITEM, PRICE_ITEM})
        public @interface ViewType {
        }

        public static final int HEADER_ITEM = 0;
        public static final int PRICE_ITEM = 1;

        public T object;
        @ViewType
        public int viewType;

        public TicketItem(T object, int viewType) {
            this.object = object;
            this.viewType = viewType;
        }
    }

    private static class PriceViewHolder extends RecyclerView.ViewHolder {
        private final TextView descriptionTextView;
        private final TextView priceTextView;
        private final TextView zonesTextView;

        public PriceViewHolder(View itemView) {
            super(itemView);
            descriptionTextView = (TextView) itemView.findViewById(R.id.ticket_description);
            priceTextView = (TextView) itemView.findViewById(R.id.ticket_price);
            zonesTextView = (TextView) itemView.findViewById(R.id.ticket_zones);
        }

        public static RecyclerView.ViewHolder create(LayoutInflater inflater, ViewGroup parent) {
            return new PriceViewHolder(inflater.inflate(R.layout.item_ticket_price, parent, false));
        }

        public static void bindTo(final PriceViewHolder holder, TicketPrice ticket) {
            holder.descriptionTextView.setText(ticket.description);
            holder.priceTextView.setText(ticket.getPrice());
            holder.zonesTextView.setText(ticket.zones);
        }
    }
} public class AdProxy {

  private final static String TAG = "AdProxy";

  private final Context mContext;
  private final boolean hasConsent;
  private final UserConsentForm userConsentForm;
  private AdView mAdView;


  public AdProxy(Context context, String id) {
    mContext = context;

    mAdView = createAdView(id);


    final SharedPreferences sharedPreferences = PreferenceManager
            .getDefaultSharedPreferences(context);
    hasConsent = sharedPreferences.getBoolean("has_consent_to_serve_personalized_ads", false);
    userConsentForm = new UserConsentForm(context);
  }

  public void onDestroy() {
    if (mAdView == null) {
      return;
    }

    mAdView.destroy();
  }

  public void onPause() {
    if (mAdView == null) {
      return;
    }

    mAdView.pause();
  }

  public void onResume() {
    if (mAdView == null) {
      return;
    }

    mAdView.resume();
  }

  public void load() {
    userConsentForm.showIfConsentIsUnknown();

    AdRequest.Builder builder = new AdRequest.Builder();
    if (BuildConfig.DEBUG) {
      String[] testDevices = AppConfig.ADMOB_TEST_DEVICES;
      for (String device : testDevices) {
        builder.addTestDevice(device);
      }
    }
    // If we don't have user consent request non-personalized ads.
    if (!hasConsent) {
      Bundle extras = new Bundle();
      extras.putString("npa", "1");
      builder.addNetworkExtrasBundle(AdMobAdapter.class, extras);
    }

    AdRequest adRequest = builder.build();

    mAdView.loadAd(adRequest);
  }

  public View getAdView() {
    return mAdView;
  }

  public ViewGroup getAdWithContainer(ViewGroup root, boolean attachToRoot) {
    int containerId = R.layout.ad_container;

    RelativeLayout mAdContainer = (RelativeLayout) LayoutInflater.from(mContext).inflate(
        containerId, root, attachToRoot);

    if (mAdView != null) {
      mAdContainer.addView(mAdView);
    }
    return mAdContainer;
  }

  @Nullable private AdView createAdView(String id) {
    try {
      return createAdMobAdView(id);
    } catch (Throwable t) {
      Log.e(TAG, "Failed to initialize AdView", t);
      Crashlytics.log("Failed to initialize AdView");
      Crashlytics.logException(t);
      return null;
    }
  }

  private AdView createAdMobAdView(String id) {
    AdView adView = new AdView(mContext);
    adView.setAdSize(AdSize.LARGE_BANNER);
    adView.setAdUnitId(id);
    adView.setAdListener(new AdListener() {

      @Override
      public void onAdLoaded() {
        Log.d(TAG, "Ad loaded");
      }

      @Override
      public void onAdFailedToLoad(int errorCode) {
        // Code to be executed when an ad request fails.
        Log.w(TAG, "Ad failed to load");
      }

      @Override
      public void onAdOpened() {
        // Code to be executed when an ad opens an overlay that
        // covers the screen.
      }

      @Override
      public void onAdLeftApplication() {
        // Code to be executed when the user has left the app.
      }

      @Override
      public void onAdClosed() {
        // Code to be executed when when the user is about to return
        // to the app after tapping on an ad.
      }
    });
    return adView;
  }

} public class Analytics {

    private static String ACTION_CLICK = "Click";

    private static Analytics sInstance;
    private final Context mContext;

    public Analytics(final Context context) {
        mContext = context;
    }

    public static Analytics getInstance(final Context context) {
        if (sInstance == null) {
            sInstance = new Analytics(context);
        }
        return sInstance;
    }

    public void registerScreen(final String screenName) {
        // Noop
    }

    public void event(final String category, final String action) {
        event(category, action, null);
    }

    public void event(final String category, final String action, final String label) {
        // Noop
    }

} public class DateTimeUtil {
    private static final String TAG = "DateTimeUtil";
    public static final long SECOND_IN_MILLIS = 1000;
    public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
    public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
    public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
    public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
    private static final SimpleDateFormat HOUR_MINUTE_FORMAT =
            new SimpleDateFormat("HH:mm", Locale.US);
    private static final SimpleDateFormat DATE_TIME_FORMAT =
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
    private static final SimpleDateFormat DATE_TIME_FORMAT_2445 =
            new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US);


    public static String format2445(Date date) {
        return DATE_TIME_FORMAT_2445.format(date);
    }

    public static Date parse2445(String dateStr) {
        try {
            return DATE_TIME_FORMAT_2445.parse(dateStr);
        } catch (ParseException e) {
            return null;
        }
    }

    /**
     * Constructs a Date from the provided date and time.
     *
     * @param dateString In the yy.MM.dd format
     * @param timeString In the HH:mm format
     * @return A Date or null if failed to process the provided strings.
     */
    public static Date fromSlDateTime(final String dateString, final String timeString) {
        String dateTime = String.format("%s %s", dateString, timeString);
        Date date = null;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yy HH:mm", Locale.US);
        try {
            date = simpleDateFormat.parse(dateTime);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    /**
     * Constructs a Date from the provided date and time.
     *
     * @param dateString In the yyyy-MM-dd format
     * @param timeString In the HH:mm format
     * @return A Date or null if failed to process the provided strings.
     */
    public static Date fromDateTime(final String dateString, final String timeString) {
        String dateTime = String.format("%s %s", dateString, timeString);
        Date date = null;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
        try {
            date = simpleDateFormat.parse(dateTime);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    /**
     * Constructs a Date from the provided date and time.
     *
     * @param dateTimeString In the yyyy-MM-ddTHH:mm format
     * @return A Date or null if failed to process the provided strings.
     */
    public static Date fromDateTime(@Nullable final String dateTimeString) {
        if (TextUtils.isEmpty(dateTimeString)) {
            return null;
        }
        Date date = null;
        try {
            date = DATE_TIME_FORMAT.parse(dateTimeString);
        } catch (ParseException e) {
            Log.e(TAG, "Failed to parse date " + dateTimeString, e);
        }
        return date;
    }

    public static String formatDate(Date date) {
        return DATE_TIME_FORMAT.format(date);
    }

    /**
     * Return given duration in a human-friendly format. For example, "4
     * minutes" or "1 second". Returns only largest meaningful unit of time,
     * from seconds up to hours.
     * <p/>
     * From android.text.format.DateUtils
     */
    public static CharSequence formatDuration(final Resources res, final long millis) {
        if (millis >= HOUR_IN_MILLIS) {
            final int hours = (int) ((millis + 1800000) / HOUR_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_hours, hours, hours);
        } else if (millis >= MINUTE_IN_MILLIS) {
            final int minutes = (int) ((millis + 30000) / MINUTE_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_minutes, minutes, minutes);
        } else {
            final int seconds = (int) ((millis + 500) / SECOND_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_seconds, seconds, seconds);
        }
    }

    public static CharSequence formatShortDuration(final Resources res, final long millis) {
        if (millis >= HOUR_IN_MILLIS) {
            final int hours = (int) ((millis + 1800000) / HOUR_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_short_hours, hours, hours);
        } else if (millis >= MINUTE_IN_MILLIS) {
            final int minutes = (int) ((millis + 30000) / MINUTE_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_short_minutes, minutes, minutes);
        } else {
            final int seconds = (int) ((millis + 500) / SECOND_IN_MILLIS);
            return res.getQuantityString(R.plurals.duration_seconds, seconds, seconds);
        }
    }

    /**
     * Format duration without rounding. Seconds are still rounded though.
     *
     * @param resources a resource
     * @param millis    duration in millis
     * @return A string representing the duration
     */
    public static CharSequence formatDetailedDuration(@NonNull final Resources resources, long millis) {
        int days = 0, hours = 0, minutes = 0;
        if (millis > 0) {
            days = (int) TimeUnit.MILLISECONDS.toDays(millis);
            millis -= TimeUnit.DAYS.toMillis(days);
            hours = (int) TimeUnit.MILLISECONDS.toHours(millis);
            millis -= TimeUnit.HOURS.toMillis(hours);
            minutes = (int) TimeUnit.MILLISECONDS.toMinutes(millis);
        }
        if (days > 0) {
            return resources.getQuantityString(R.plurals.duration_days, days, days);
        }
        if (hours > 0) {
            if (minutes == 0) {
                return resources.getQuantityString(R.plurals.duration_short_hours, hours, hours);
            }
            return resources.getString(R.string.duration_short_hours_minutes, hours, minutes);
        }
        return resources.getQuantityString(R.plurals.duration_short_minutes, minutes, minutes);
    }

    /**
     * Formats the passed display time to something translated and human readable.
     *
     * @param displayTime The display time, can be null, Nu, HH:MM or in the M min.
     * @param context     A contect
     * @return
     */
    public static CharSequence formatDisplayTime(@Nullable String displayTime,
                                                 @NonNull Context context) {
        // time str can be "Nu", "2 min" or in the "12:00" format, and possible some unknown
        // ones as well.

        // Maybe we should use some memoization func for this.

        // Catch when we should show "Now"
        if (TextUtils.isEmpty(displayTime)
                || TextUtils.equals(displayTime, "0 min")
                || TextUtils.equals(displayTime, "Nu")) {
            return context.getString(R.string.now);
        }
        // Catch "2 min"
        if (displayTime.contains("min")) {
            String minutes = displayTime.replace(" min", "");
            // Try to convert minutes to millis.
            try {
                long minutesConverted = Long.valueOf(minutes);
                return formatShortDuration(context.getResources(), minutesConverted * 60000);
            } catch (NumberFormatException e) {
                return displayTime;
            }
        }
        // Catch the "HH:MM" format.
        try {
            Date date = HOUR_MINUTE_FORMAT.parse(displayTime);
            return DateFormat.getTimeFormat(context).format(date);
        } catch (ParseException e) {
            return displayTime;
        }
    }

    public static CharSequence routeToTimeDisplay(Context context, Route route) {
        java.text.DateFormat format = android.text.format.DateFormat.getTimeFormat(context);
        BidiFormatter bidiFormatter = BidiFormatter.getInstance(RtlUtils.isRtl(Locale.getDefault()));

        Pair<Date, RealTimeState> departsAt = route.departsAt(true);
        Pair<Date, RealTimeState> arrivesAt = route.arrivesAt(true);

        String departsAtStr = format.format(departsAt.first);
        String arrivesAtStr = format.format(arrivesAt.first);
        CharSequence displayTime;
        if (!DateUtils.isToday(departsAt.first.getTime())) {
            displayTime = String.format("%s %s – %s",
                    bidiFormatter.unicodeWrap(DateUtils.getRelativeTimeSpanString(
                            departsAt.first.getTime(), System.currentTimeMillis(),
                            DateUtils.DAY_IN_MILLIS).toString()),
                    bidiFormatter.unicodeWrap(departsAtStr),
                    bidiFormatter.unicodeWrap(arrivesAtStr));
        } else {
            displayTime = String.format("%s – %s",
                    bidiFormatter.unicodeWrap(departsAtStr),
                    bidiFormatter.unicodeWrap(arrivesAtStr));
        }

        ForegroundColorSpan spanDepartsAt = new ForegroundColorSpan(ContextCompat.getColor(
                context, ViewHelper.getTextColorByRealtimeState(departsAt.second)));
        Pattern patternDepartsAt = Pattern.compile(departsAtStr);
        displayTime = SpanUtils.createSpannable(displayTime, patternDepartsAt, spanDepartsAt);

        ForegroundColorSpan spanArrivessAt = new ForegroundColorSpan(ContextCompat.getColor(
                context, ViewHelper.getTextColorByRealtimeState(arrivesAt.second)));
        Pattern patternArrivesAt = Pattern.compile(arrivesAtStr);
        displayTime = SpanUtils.createSpannable(displayTime, patternArrivesAt, spanArrivessAt);

        return displayTime;
    }

    /**
     * Get the delay between a scheduled and expected time.
     * <p/>
     * If running ahead of schedule a negative delay is returned.
     *
     * @param scheduledDateTime The scheduled time
     * @param expectedDateTime The expected time
     * @return Delay in seconds
     */
    public static int getDelay(Date scheduledDateTime, Date expectedDateTime) {
        if (scheduledDateTime == null || expectedDateTime == null) {
            throw new IllegalArgumentException("Scheduled and or excepted date must not be null");
        }
        int delay = (int) (Math.abs(TimeUnit.MILLISECONDS.toSeconds(
                expectedDateTime.getTime() - scheduledDateTime.getTime())));
        if (expectedDateTime.getTime() < scheduledDateTime.getTime()) {
            return -delay;
        }
        return delay;
    }

    /**
     * Get the real-time state based on the passed delay.
     * @param delay The current delay
     * @return The real-time state
     */
    public static RealTimeState getRealTimeStateFromDelay(int delay) {
        if (delay > 0) {
            return RealTimeState.BEHIND_SCHEDULE;
        } else if (delay < 0) {
            return RealTimeState.AHEAD_OF_SCHEDULE;
        }
        return RealTimeState.ON_TIME;
    }
} public class LegUtil {
    public static Drawable getTransportDrawable(Context context, Leg leg) {
        int color = ContextCompat.getColor(context, R.color.icon_default);
        Drawable drawable;

        switch (leg.getTravelMode()) {
            case "bus":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_bus_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case "metro":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_sl_metro);
                return ViewHelper.tintIcon(drawable, color);
            case "foot":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_walk_20dp);
                return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.icon_default));
            case "train":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case "tram":
            case "lightTrain":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_light_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case "boat":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_boat_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case "car":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_car_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case "bike":
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_bike_20dp);
                return ViewHelper.tintIcon(drawable, color);
            default:
                // What to use when we don't know..
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
        }
        return ViewHelper.tintIcon(drawable, color);
    }

    @ColorInt
    public static int getColor(Context context, Leg leg) {
        if (leg.getRouteColor() == null) {
            // Add color for foot.
            return ContextCompat.getColor(context, R.color.train);
        }
        return Color.parseColor(leg.getRouteColor());
    }

    public static String getRouteName(Leg leg, boolean truncate) {
        String routeName = leg.getRouteName();
        if (!TextUtils.isEmpty(routeName)) {
            routeName = ViewHelper.uppercaseFirst(routeName, Locale.US);
        } else {
            routeName = leg.getRouteShortName();
        }
        if (TextUtils.isEmpty(routeName)) {
            return "";
        }

        if (truncate && routeName.length() > 30) {
            routeName = String.format(Locale.US, "%s…", routeName.trim().substring(0, 29));
        }
        return routeName;
    }

    public static List<TravelMode> transportModesToTravelModes(List<String> transportModes) {
        List<TravelMode> travelModes = new ArrayList<>();
        for (String transportMode : transportModes) {
            switch (transportMode) {
                case TransportMode.BOAT:
                case TransportMode.WAX:
                    travelModes.add(new TravelMode(TravelMode.BOAT));
                    break;
                case TransportMode.TRAIN:
                    travelModes.add(new TravelMode(TravelMode.TRAIN));
                    break;
                case TransportMode.BUS:
                    travelModes.add(new TravelMode(TravelMode.BUS));
                    break;
                case TransportMode.TRAM:
                    travelModes.add(new TravelMode(TravelMode.TRAM));
                    travelModes.add(new TravelMode(TravelMode.LIGHT_TRAIN));
                    break;
                case TransportMode.METRO:
                    travelModes.add(new TravelMode(TravelMode.METRO));
                    break;
                case TransportMode.BIKE_RENTAL:
                    travelModes.add(new TravelMode(TravelMode.BIKE_RENTAL));
                    break;
            }
        }
        return travelModes;
    }

    public static List<String> travelModesToTransportModes(List<TravelMode> travelModes) {
        List<String> transportModes = new ArrayList<>();
        for (TravelMode travelMode : travelModes) {
            switch (travelMode.getMode()) {
                case TravelMode.BOAT:
                    transportModes.add(TransportMode.BOAT);
                    transportModes.add(TransportMode.WAX);
                    break;
                case TravelMode.TRAIN:
                    transportModes.add(TransportMode.TRAIN);
                    break;
                case TravelMode.BUS:
                    transportModes.add(TransportMode.BUS);
                    break;
                case TravelMode.TRAM:
                case TravelMode.LIGHT_TRAIN:
                    transportModes.add(TransportMode.TRAM);
                    break;
                case TravelMode.METRO:
                    transportModes.add(TransportMode.METRO);
                    break;
            }
        }
        return transportModes;
    }
} public class Monitor {
    private static final String TAG = "Monitor";
    private Handler handler = new Handler();
    private boolean isStarted;


    public void onStart() {
        isStarted = true;
        handler.removeCallbacks(ticker);
        ticker.run();
    }

    public void onStop() {
        handler.removeCallbacks(ticker);
        isStarted = false;
    }

    public void handleUpdate() {
    }

    protected void onUpdate() {
        if (this.isStarted) {
            handleUpdate();
        } else {
            onStop();
            Log.e(TAG, "updated called but manager is not running.");
        }
    }

    private final Runnable ticker = new Runnable() {
        public void run() {
            onUpdate();

            long now = SystemClock.uptimeMillis();
            long next = now + 60 * 1000;

            if (isStarted) {
                handler.postAtTime(ticker, next);
            }
        }
    };

} public class PolyUtil {

    private PolyUtil() {}

    /**
     * Decodes an encoded path string into a sequence of LatLngs.
     */
    public static List<LatLng> decode(final String encodedPath) {
        int len = encodedPath.length();

        // For speed we preallocate to an upper bound on the final length, then
        // truncate the array before returning.
        final List<LatLng> path = new ArrayList<LatLng>();
        int index = 0;
        int lat = 0;
        int lng = 0;

        while (index < len) {
            int result = 1;
            int shift = 0;
            int b;
            do {
                b = encodedPath.charAt(index++) - 63 - 1;
                result += b << shift;
                shift += 5;
            } while (b >= 0x1f);
            lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

            result = 1;
            shift = 0;
            do {
                b = encodedPath.charAt(index++) - 63 - 1;
                result += b << shift;
                shift += 5;
            } while (b >= 0x1f);
            lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

            path.add(new LatLng(lat * 1e-5, lng * 1e-5));
        }

        return path;
    }
} public class RtlUtils {

    private static final Set<String> RTL_LANGUAGES;

    static {
        Set<String> lang = new HashSet<String>();
        lang.add("ar"); // Arabic
        lang.add("dv"); // Divehi
        lang.add("fa"); // Persian (Farsi)
        lang.add("ha"); // Hausa
        lang.add("he"); // Hebrew
        lang.add("iw"); // Hebrew (old code)
        lang.add("ji"); // Yiddish (old code)
        lang.add("ps"); // Pashto, Pushto
        lang.add("ur"); // Urdu
        lang.add("yi"); // Yiddish
        RTL_LANGUAGES = Collections.unmodifiableSet(lang);
    }

    public static boolean isRtl(Locale locale) {
        return locale != null && RTL_LANGUAGES.contains(locale.getLanguage());
    }

    public static boolean isRtl(CharSequence s) {
        if (s == null || s.length() == 0) {
            return false;
        }
        switch(Character.getDirectionality(s.charAt(0))) {
            case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
            case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
                return true;
            default:
                return false;
        }
    }
} public class ViewHelper {
    /**
     * Get the height of the display in pixels
     *
     * @param context The Context
     * @return Height of the display in pixels
     */
    public static int getDisplayHeight(Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return displayMetrics.heightPixels;
    }

    /**
     * Get the width of the display in pixels
     *
     * @param context The Context
     * @return Width of the display in pixels
     */
    public static int getDisplayWidth(Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return displayMetrics.widthPixels;
    }

    @SuppressLint("NewApi")
    public static void crossfade(final View fromView, final View toView) {
        // Set the content view to 0% opacity but visible, so that it is visible
        // (but fully transparent) during the animation.

        toView.setVisibility(View.VISIBLE);
        fromView.setVisibility(View.GONE);
        if (true) {
            return;
        }

        toView.setAlpha(0f);
        toView.setVisibility(View.VISIBLE);

        // Animate the content view to 100% opacity, and clear any animation
        // listener set on the view.
        toView.animate()
                .alpha(1f)
                .setDuration(200)
                .setListener(null);

        // Animate the loading view to 0% opacity. After the animation ends,
        // set its visibility to GONE as an optimization step (it won't
        // participate in layout passes, etc.)
        fromView.animate()
                .alpha(0f)
                .setDuration(200)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        fromView.setVisibility(View.GONE);
                    }
                });
    }

    public static Drawable tintIcon(Resources res, Drawable d) {
        return tintIcon(d, res.getColor(R.color.icon_default_inverse));
    }

    public static Drawable tintIcon(Drawable d, @ColorInt int color) {
        Drawable m = d.mutate();
        m.setColorFilter(color, PorterDuff.Mode.SRC_IN);
        m.setAlpha(255);
        return d;
    }

    public static void tint(@NonNull ImageView v, @ColorInt int color) {
        v.setImageDrawable(tintIcon(v.getDrawable(), color));
    }

    public static Drawable getDrawableColorInt(Context context, @DrawableRes int drawableRes,
                                               @ColorInt int color) {
        return tintIcon(ContextCompat.getDrawable(context, drawableRes), color);
    }

    public static Drawable getDrawableColorRes(Context context, @DrawableRes int drawableRes,
                                               @ColorRes int color) {
        return tintIcon(
                ContextCompat.getDrawable(context, drawableRes),
                ContextCompat.getColor(context, color));
    }

    public static int getColorFromAttr(Context context, @AttrRes int attrColor) {
        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(attrColor, typedValue, true);
        return typedValue.data;
    }

    public static void setText(@NonNull TextView v, @NonNull CharSequence t) {
        if (!t.equals(v.getText())) {
            v.setText(t);
        }
    }

    public static void flipIfRtl(View view) {
        if (RtlUtils.isRtl(Locale.getDefault())) {
            ViewCompat.setScaleX(view, -1f);
        }
    }

    /**
     * Helper method to convert dips to pixels.
     */
    public static int dipsToPix(final Resources res, final float dps) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dps,
                res.getDisplayMetrics());
    }

    /**
     * Uppercase the first letter of the passed str.
     *
     * @param str String to uppercase
     * @return str with the first letter uppercase
     */
    public static String uppercaseFirst(final String str, final Locale locale) {
        return str.substring(0, 1).toUpperCase(locale) + str.substring(1);
    }

    public static String getLineName(int transportMode, String lineNumber) {
        // Will be moved to the new API.
        if (TextUtils.isEmpty(lineNumber)) {
            return null;
        }
        switch (transportMode) {
            case TransportMode.TRAM_INDEX:
                if ("7".equals(lineNumber)
                        || "12".equals(lineNumber)
                        || "22".equals(lineNumber)
                        || "21".equals(lineNumber)) {
                    return "S" + lineNumber;
                }
                return "L" + lineNumber;
            case TransportMode.METRO_INDEX:
                return "T" + lineNumber;
            case TransportMode.TRAIN_INDEX:
                return "J" + lineNumber;
            case TransportMode.BUS_INDEX:
                return "B" + lineNumber;
        }
        return lineNumber;
    }

    @ColorInt
    public static int getLineColor(Context context, int transportMode,
                                   String lineNumber, String lineName) {
        switch (transportMode) {
            case TransportMode.TRAM_INDEX:
                if ("22".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_l22);
                } else if ("12".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_l12);
                } else if ("21".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_l21);
                } else if ("25".equals(lineNumber)
                        || "26".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_l2526);
                } else if ("27".equals(lineNumber) || "27S".equals(lineNumber)
                        || "28".equals(lineNumber) || "28S".equals(lineNumber)
                        || "29".equals(lineNumber) || "29S".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_l272829);
                }
                return ContextCompat.getColor(context, R.color.train);
            case TransportMode.METRO_INDEX:
                if ("17".equals(lineNumber)
                        || "18".equals(lineNumber)
                        || "19".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.metro_green);
                } else if ("13".contains(lineNumber)
                        || "14".contains(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.metro_red);
                } else if ("10".contains(lineNumber)
                        || "11".contains(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.metro_blue);
                }
            case TransportMode.TRAIN_INDEX:
                if ("35".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_j35);
                } else if ("36".equals(lineNumber)
                        || "37".equals(lineNumber)
                        || "38".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_j363738);
                }
                return ContextCompat.getColor(context, R.color.train);
            case TransportMode.BUS_INDEX:
                if ("1".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_b1);
                } else if ("2".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_b2);
                } else if ("3".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_b3);
                } else if ("4".equals(lineNumber)) {
                    return ContextCompat.getColor(context, R.color.traffic_type_b4);
                }
                if (lineName != null && lineName.contains("blå")) {
                    return ContextCompat.getColor(context, R.color.traffic_type_b1);
                }
                return ContextCompat.getColor(context, R.color.bus_red);
            case TransportMode.BOAT_INDEX:
                return ContextCompat.getColor(context, R.color.traffic_type_boat);
        }
        return ContextCompat.getColor(context, R.color.train);
    }

    public static Drawable getColoredDrawableForTransport(Context context, int transportMode,
                                                   String lineName, String lineNumber) {
        int color = ViewHelper.getLineColor(context, transportMode, lineNumber, lineName);
        Drawable drawable;
        switch (transportMode) {
            case TransportMode.BUS_INDEX:
            case TransportMode.NAR_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_bus_20dp);
                if (lineName.contains("blå")) {
                    return ViewHelper.tintIcon(drawable, color);
                }
                return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.bus_red));
            case TransportMode.METRO_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_sl_metro);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.FOOT_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_walk_20dp);
                return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.icon_default));
            case TransportMode.TRAIN_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.TRAM_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_light_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.BOAT_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_boat_20dp);
                return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.traffic_type_b4));
            default:
                // What to use when we don't know..
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
        }
        return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.train));
    }

    public static Drawable getDrawableForTransport(Context context, int transportMode,
                                                   String lineName, String lineNumber) {
        int color = ContextCompat.getColor(context, R.color.icon_default);
        Drawable drawable;
        switch (transportMode) {
            case TransportMode.BUS_INDEX:
            case TransportMode.NAR_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_bus_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.METRO_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_sl_metro);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.FOOT_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_walk_20dp);
                return ViewHelper.tintIcon(drawable, ContextCompat.getColor(context, R.color.icon_default));
            case TransportMode.TRAIN_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.TRAM_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_light_train_20dp);
                return ViewHelper.tintIcon(drawable, color);
            case TransportMode.BOAT_INDEX:
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_boat_20dp);
                return ViewHelper.tintIcon(drawable, color);
            default:
                // What to use when we don't know..
                drawable = ContextCompat.getDrawable(context, R.drawable.ic_transport_train_20dp);
        }
        return ViewHelper.tintIcon(drawable, color);
    }

    public static @ColorRes int getTextColorByRealtimeState(RealTimeState realTimeState) {
        switch (realTimeState) {
            case AHEAD_OF_SCHEDULE:
            case ON_TIME:
                return R.color.schedule_ahead;
            case BEHIND_SCHEDULE:
                return R.color.schedule_late;
        }
        return R.color.body_text_1;
    }

    public static void setTextColorForTimeView(TextView textView, Leg leg, boolean isDeparture) {
        textView.setTextColor(ContextCompat.getColor(textView.getContext(),
                getTextColorByRealtimeState(leg.realtimeState(isDeparture))));
    }
} public class RoundedBackgroundSpan extends ReplacementSpan {
    private int mPadding = 0;
    @ColorInt
    private int mBackgroundColor;
    @ColorInt
    private int mTextColor;

    public RoundedBackgroundSpan(@ColorInt int backgroundColor, @ColorInt int textColor, int  padding) {
        super();
        mBackgroundColor = backgroundColor;
        mTextColor = textColor;
        mPadding = padding;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return (int) paint.measureText(text.subSequence(start, end).toString()) + mPadding * 2;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x, int top, int y, int bottom, Paint paint) {
        float width = paint.measureText(text.subSequence(start, end).toString());

        int newTop = (int) (bottom - paint.getTextSize() - paint.descent());
        RectF rect = new RectF(x, newTop, x + width + mPadding * 2, bottom);
        paint.setColor(mBackgroundColor);
        canvas.drawRoundRect(rect, mPadding, mPadding, paint);
        paint.setColor(mTextColor);
        paint.setFakeBoldText(true);

        float textY = bottom - paint.descent();
        canvas.drawText(text, start, end, x + mPadding, textY, paint);
    }
} public final class SpanUtils {
    private SpanUtils() {
    }

    public static CharSequence createSpannable(@NonNull CharSequence source, @NonNull ReplacementSpan style) {
        SpannableStringBuilder sb = new SpannableStringBuilder(source);
        sb.setSpan(style, 0, source.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return sb;
    }

    public static CharSequence createSpannable(CharSequence source, Pattern pattern, ReplacementSpan style) {
        SpannableStringBuilder sb = new SpannableStringBuilder(source);

        Matcher matcher = pattern.matcher(source);
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            sb.setSpan(style, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        }

        return sb;
    }

    public static CharSequence createSpannable(Context context, int stringId, Pattern pattern, CharacterStyle... styles) {
        String string = context.getString(stringId);
        return createSpannable(string, pattern, styles);
    }

    public static CharSequence createSpannable(CharSequence source, Pattern pattern, CharacterStyle... styles) {
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
        Matcher matcher = pattern.matcher(source);
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            applyStylesToSpannable(spannableStringBuilder, start, end, styles);
        }
        return spannableStringBuilder;
    }

    private static SpannableStringBuilder applyStylesToSpannable(SpannableStringBuilder source, int start, int end, CharacterStyle... styles) {
        for (CharacterStyle style : styles) {
            source.setSpan(CharacterStyle.wrap(style), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return source;
    }
} public static final String ADMOB_ROUTE_DETAILS_AD_UNIT_ID = BuildConfig.ADMOB_ROUTE_DETAILS_AD_UNIT_ID;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Paint;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.ColorInt;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.core.util.Pair;
import androidx.core.view.ViewCompat;
import androidx.appcompat.app.ActionBar;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.markupartist.sthlmtraveling.data.api.ApiService;
import com.markupartist.sthlmtraveling.data.models.Alert;
import com.markupartist.sthlmtraveling.data.models.IntermediateResponse;
import com.markupartist.sthlmtraveling.data.models.IntermediateStop;
import com.markupartist.sthlmtraveling.data.models.Leg;
import com.markupartist.sthlmtraveling.data.models.Place;
import com.markupartist.sthlmtraveling.data.models.RealTimeState;
import com.markupartist.sthlmtraveling.data.models.Route;
import com.markupartist.sthlmtraveling.data.models.TravelMode;
import com.markupartist.sthlmtraveling.provider.JourneysProvider.Journey.Journeys;
import com.markupartist.sthlmtraveling.provider.planner.JourneyQuery;
import com.markupartist.sthlmtraveling.provider.site.Site;
import com.markupartist.sthlmtraveling.ui.models.LegViewModel;
import com.markupartist.sthlmtraveling.ui.view.TicketDialogFragment;
import com.markupartist.sthlmtraveling.utils.AdProxy;
import com.markupartist.sthlmtraveling.utils.Analytics;
import com.markupartist.sthlmtraveling.utils.DateTimeUtil;
import com.markupartist.sthlmtraveling.utils.LegUtil;
import com.markupartist.sthlmtraveling.utils.Monitor;
import com.markupartist.sthlmtraveling.utils.PolyUtil;
import com.markupartist.sthlmtraveling.utils.RtlUtils;
import com.markupartist.sthlmtraveling.utils.ViewHelper;
import com.markupartist.sthlmtraveling.utils.text.RoundedBackgroundSpan;
import com.markupartist.sthlmtraveling.utils.text.SpanUtils;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import retrofit.Callback;
import retrofit.RetrofitError;
import retrofit.client.Response;
import static com.markupartist.sthlmtraveling.AppConfig.ADMOB_ROUTE_DETAILS_AD_UNIT_ID;
/*
 * Copyright (C) 2009 Johan Nilsson <http://markupartist.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.markupartist.sthlmtraveling;







public class RouteDetailActivity extends BaseListActivity implements OnMapReadyCallback {
    public static final String TAG = "RouteDetailActivity";

    public static final String EXTRA_ROUTE = "sthlmtraveling.intent.extra.ROUTE";
    public static final String EXTRA_JOURNEY_QUERY = "sthlmtraveling.intent.action.JOURNEY_QUERY";
    private static final String STATE_LEGS = "sthlmtraveling.intent.state.LEGS";

    private Route mRoute;
    private JourneyQuery mJourneyQuery;
    private SubTripAdapter mSubTripAdapter;

    private ImageButton mFavoriteButton;

    private ActionBar mActionBar;
    private AdProxy mAdProxy;
    private ApiService mApiService;
    private Monitor mMonitor;
    private View mFooterView;
    private GoogleMap map;

    void setupMapHeader() {
        View layout = getLayoutInflater().inflate(R.layout.route_map_row, null);
        getListView().addHeaderView(layout, null, false);
        MapView mapView = layout.findViewById(R.id.lite_map);
        if (mapView != null) {
            mapView.onCreate(null);
            mapView.getMapAsync(this);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.route_details_list);

        registerScreen("Route details");

        Bundle extras = getIntent().getExtras();

        mRoute = extras.getParcelable(EXTRA_ROUTE);

        mApiService = MyApplication.get(RouteDetailActivity.this).getApiService();

        mJourneyQuery = extras.getParcelable(EXTRA_JOURNEY_QUERY);

        mActionBar = initActionBar();

        updateStartAndEndPointViews(mJourneyQuery);

        View headerView = getLayoutInflater().inflate(R.layout.route_header_details, null);

        if (shouldShowAds(mJourneyQuery.hasPromotions)) {
            mAdProxy = new AdProxy(this, ADMOB_ROUTE_DETAILS_AD_UNIT_ID);
        }

        ViewGroup adContainer = null;
        if (mAdProxy != null) {
            adContainer = mAdProxy.getAdWithContainer(getListView(), false);
            mAdProxy.load();
        }

        TextView timeView = (TextView) headerView.findViewById(R.id.route_date_time);

        BidiFormatter bidiFormatter = BidiFormatter.getInstance(Locale.getDefault());
        String timeStr = getString(R.string.time_to,
                bidiFormatter.unicodeWrap(String.valueOf(DateTimeUtil.formatDetailedDuration(getResources(), mRoute.getDuration() * 1000))),