From 3458853f5ae138ff078f6ab0d0f7858d022a6341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sat, 8 Aug 2026 17:10:34 -0400 Subject: [PATCH 01/17] GTFS Static > agency timezone > fix infrequent issue Happened today in Prod, AI think it might come from static date format instances containing old timezone. --- .../gtfs/GTFSScheduleTimestampsProvider.java | 24 ++-- .../provider/gtfs/GTFSStatusProvider.java | 116 +++++++++--------- 2 files changed, 72 insertions(+), 68 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index 3363bc62..ce28e624 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -9,7 +9,6 @@ import androidx.annotation.Nullable; import org.mtransit.android.commons.MTLog; -import org.mtransit.android.commons.ThreadSafeDateFormatter; import org.mtransit.android.commons.TimeUtils; import org.mtransit.android.commons.data.RouteDirectionStop; import org.mtransit.android.commons.data.Schedule; @@ -20,6 +19,7 @@ import org.mtransit.android.commons.provider.scheduletimestamp.ScheduleTimestampsProviderContract; import org.mtransit.commons.FeatureFlags; +import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Set; @@ -47,9 +47,11 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro final long startsAtInMs = filter.getStartsAtInMs(); final long endsAtInMs = filter.getEndsAtInMs(); final Context context = provider.requireContextCompat(); - final ThreadSafeDateFormatter dateFormat = GTFSStatusProvider.getDateFormat(context); - final ThreadSafeDateFormatter timeFormat = GTFSStatusProvider.getTimeFormat(context); - final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getAgencyTimeZoneId(context)); + final String agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); + final TimeZone timeZone = TimeZone.getTimeZone(agencyTimeZoneId); + final DateFormat dateFormat = GTFSStatusProvider.getNewDateFormat(timeZone); + final DateFormat timeFormat = GTFSStatusProvider.getNewTimeFormat(timeZone); + final DateFormat dateAndTimeFormat = GTFSStatusProvider.getNewDateAndTimeFormat(timeZone); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, startsAtInMs); startsAt.add(Calendar.DATE, -1); // starting yesterday Set dayTimestamps; @@ -61,8 +63,8 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro while (startsAt.getTimeInMillis() <= endsAtInMs) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); GTFSStatusProvider.alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); - lookupDayTime = timeFormat.formatThreadSafe(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt); + lookupDayTime = timeFormat.format(lookupStartAt); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + GTFSStatusProvider.TWENTY_FOUR_HOURS); } else { // ELSE IF tomorrow or later DO @@ -75,12 +77,14 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro rds.getStop().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat, + agencyTimeZoneId ); if (startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule && dayTimestamps.isEmpty()) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once dayTimestamps = GTFSStatusProvider.findScheduleList( provider, rds.getRoute().getId(), @@ -88,7 +92,9 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro rds.getStop().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat, + agencyTimeZoneId ); } dataRequests++; // 1 more data request done diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index 8854b45a..daa29dd7 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -17,7 +17,6 @@ import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.R; import org.mtransit.android.commons.SqlUtils; -import org.mtransit.android.commons.ThreadSafeDateFormatter; import org.mtransit.android.commons.TimeUtils; import org.mtransit.android.commons.data.POI; import org.mtransit.android.commons.data.POIStatus; @@ -36,6 +35,8 @@ import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -168,30 +169,19 @@ public static POIStatus getNewStatus(@NonNull GTFSProvider provider, @NonNull St private static final String DATE_FORMAT_PATTERN = "yyyyMMdd"; - @Nullable - private static ThreadSafeDateFormatter dateFormat; - @NonNull - static ThreadSafeDateFormatter getDateFormat(@NonNull Context context) { - if (dateFormat == null) { - dateFormat = new ThreadSafeDateFormatter(DATE_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); - dateFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); - } + static DateFormat getNewDateFormat(@NonNull TimeZone timeZone) { + final DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_PATTERN, Locale.ENGLISH); + dateFormat.setTimeZone(timeZone); return dateFormat; } private static final String TIME_FORMAT_PATTERN = "HHmmss"; - @Nullable - private static ThreadSafeDateFormatter timeFormat; @NonNull - static ThreadSafeDateFormatter getTimeFormat(@NonNull Context context) { - if (timeFormat == null) { - timeFormat = new ThreadSafeDateFormatter(TIME_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); - timeFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); - } + static DateFormat getNewTimeFormat(@NonNull TimeZone timeZone) { + final DateFormat timeFormat = new SimpleDateFormat(TIME_FORMAT_PATTERN, Locale.ENGLISH); + timeFormat.setTimeZone(timeZone); return timeFormat; } @@ -243,10 +233,11 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide final long timestamp = filter.getTimestampOrDefault(); // NOW final long minTimestampCoveredIntMs = timestamp + minDurationCoveredInMs; final Context context = provider.requireContextCompat(); - final ThreadSafeDateFormatter dateFormat = getDateFormat(context); - final ThreadSafeDateFormatter timeFormat = getTimeFormat(context); - final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); - final TimeZone timeZone = TimeZone.getTimeZone(localTimeZoneId); + final String agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); + final TimeZone timeZone = TimeZone.getTimeZone(agencyTimeZoneId); + final DateFormat dateFormat = getNewDateFormat(timeZone); + final DateFormat timeFormat = getNewTimeFormat(timeZone); + final DateFormat dateAndTimeFormat = getNewDateAndTimeFormat(timeZone); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, timestamp); if (lookBehindInMs > PROVIDER_PRECISION_IN_MS) { if (lookBehindInMs > 0L) { @@ -268,8 +259,8 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide while (dataRequests < maxDataRequests) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); - lookupDayTime = timeFormat.formatThreadSafe(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt); + lookupDayTime = timeFormat.format(lookupStartAt); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO @@ -284,12 +275,14 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide rds.getStop().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat, + agencyTimeZoneId ); if (dayTimestamps.isEmpty() && startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule ) { - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once dayTimestamps = findScheduleList( provider, rds.getRoute().getId(), @@ -297,7 +290,9 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide rds.getStop().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat, + agencyTimeZoneId ); } dataRequests++; // 1 more data request done @@ -327,13 +322,13 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide static void alignLookupStartTime( @Nullable Integer lastServiceDate, - @NonNull ThreadSafeDateFormatter dateFormat, + @NonNull DateFormat dateFormat, @NonNull Calendar lookupStartAt, long lastDepartureInMs ) { if (lastServiceDate != null) { try { - while (Integer.parseInt(dateFormat.formatThreadSafe(lookupStartAt)) > lastServiceDate) { + while (Integer.parseInt(dateFormat.format(lookupStartAt)) > lastServiceDate) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind } } catch (Exception e) { @@ -427,7 +422,9 @@ static Set findScheduleList( long directionId, // includes routeId, int stopId, String dateS, String timeS, - long diffWithRealityInMs + long diffWithRealityInMs, + @NonNull DateFormat dateAndTimeFormat, + @NonNull String agencyTimeZoneId ) { final int timeI = FeatureFlags.F_SCHEDULE_IN_MINUTES ? Integer.parseInt(timeS) / 100 : Integer.parseInt(timeS); Set result = new HashSet<>(); @@ -436,7 +433,6 @@ static Set findScheduleList( BufferedReader br = null; String line = null; final Context context = provider.requireContextCompat(); - final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); String fileName = String.format(getSTOP_SCHEDULE_RAW_FILE_FORMAT(context), stopId); try { @SuppressLint("DiscouragedApi") @@ -487,17 +483,17 @@ static Set findScheduleList( final int extraIdx = i * GTFS_SCHEDULE_STOP_FILE_COL_COUNT_EXTRA; lineDepartureDelta = Integer.parseInt(lineItems[GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_IDX + extraIdx]); lineDeparture += lineDepartureDelta; - tTimestampInMs = convertToTimestamp(context, lineDeparture, dateS); + tTimestampInMs = convertToTimestamp(dateAndTimeFormat, lineDeparture, dateS); if (lineDeparture > timeI) { if (tTimestampInMs != null) { - timestamp = new Schedule.Timestamp(tTimestampInMs + diffWithRealityInMs, localTimeZoneId); + timestamp = new Schedule.Timestamp(tTimestampInMs + diffWithRealityInMs, agencyTimeZoneId); if (FeatureFlags.F_EXPORT_TRIP_ID) { if (FeatureFlags.F_EXPORT_ARRIVAL_W_TRIP_ID && GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_ARRIVAL_DIFF_IDX >= 0) { departureArrivalDiffS = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_ARRIVAL_DIFF_IDX + extraIdx]; if (!TextUtils.isEmpty(departureArrivalDiffS) && CharUtils.isDigitsOnly(departureArrivalDiffS)) { departureArrivalDiff = Integer.parseInt(departureArrivalDiffS); if (departureArrivalDiff > 0) { - arrivalTimestampMs = convertToTimestamp(context, lineDeparture - departureArrivalDiff, dateS); + arrivalTimestampMs = convertToTimestamp(dateAndTimeFormat, lineDeparture - departureArrivalDiff, dateS); if (arrivalTimestampMs != null) { timestamp.setArrivalT(arrivalTimestampMs); } @@ -602,9 +598,10 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid final long timestamp = filter.getTimestampOrDefault(); // NOW final long minTimestampCovered = timestamp + minDurationCoveredInMs; final Context context = provider.requireContextCompat(); - final ThreadSafeDateFormatter dateFormat = getDateFormat(context); - final ThreadSafeDateFormatter timeFormat = getTimeFormat(context); final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getAgencyTimeZoneId(context)); + final DateFormat dateFormat = getNewDateFormat(timeZone); + final DateFormat timeFormat = getNewTimeFormat(timeZone); + final DateFormat dateAndTimeFormat = getNewDateAndTimeFormat(timeZone); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, timestamp); startsAt.add(Calendar.DATE, -1); // starting yesterday HashSet dayFrequencies; @@ -616,8 +613,8 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid while (dataRequests < maxDataRequests) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); - lookupDayTime = timeFormat.formatThreadSafe(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt); + lookupDayTime = timeFormat.format(lookupStartAt); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO @@ -631,21 +628,23 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid rds.getDirection().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat ); if (dayFrequencies.isEmpty() && startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule && MIDNIGHT.equals(lookupDayTime) // not a partial schedule ) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind - lookupDayDate = dateFormat.formatThreadSafe(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once dayFrequencies = findFrequencyList( provider, rds.getRoute().getId(), rds.getDirection().getId(), lookupDayDate, lookupDayTime, - startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis() + startsAt.getTimeInMillis() - lookupStartAt.getTimeInMillis(), + dateAndTimeFormat ); } dataRequests++; // 1 more data request done @@ -663,10 +662,15 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid } @NonNull - private static HashSet findFrequencyList(@NonNull GTFSProvider provider, - long routeId, long directionId, - String dateS, String timeS, - long diffWithRealityInMs) { + private static HashSet findFrequencyList( + @NonNull GTFSProvider provider, + long routeId, + long directionId, + String dateS, + String timeS, + long diffWithRealityInMs, + @NonNull DateFormat dateAndTimeFormat + ) { long timeI = Long.parseLong(timeS); final HashSet result = new HashSet<>(); final Set> serviceIdOrIntAndExceptionTypes = findServicesAndExceptionTypes(provider, dateS); @@ -714,8 +718,8 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid endTime = Integer.parseInt(lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_END_TIME_IDX]); if (timeI <= endTime) { startTime = Integer.parseInt(lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_START_TIME_IDX]); - tStartTimeInMs = convertToTimestamp(context, startTime, dateS); - tEndTimeInMs = convertToTimestamp(context, endTime, dateS); + tStartTimeInMs = convertToTimestamp(dateAndTimeFormat, startTime, dateS); + tEndTimeInMs = convertToTimestamp(dateAndTimeFormat, endTime, dateS); tHeadway = Integer.parseInt(lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_HEADWAY_IDX]); //noinspection ConstantConditions if (tStartTimeInMs != null && tEndTimeInMs != null && tHeadway != null) { @@ -742,12 +746,12 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid private static final String TIME_FORMATTER = "%06d"; @Nullable - private static Long convertToTimestamp(Context context, int timeInt, String dateS) { + private static Long convertToTimestamp(@NonNull DateFormat dateAndTimeFormat, int timeInt, String dateS) { try { if (FeatureFlags.F_SCHEDULE_IN_MINUTES) { timeInt *= 100; // HHMM -> HHMMSS } - final Date parsedDate = getToTimestampFormat(context).parseThreadSafe( + final Date parsedDate = dateAndTimeFormat.parse( dateS + String.format(Locale.ENGLISH, TIME_FORMATTER, timeInt) ); return parsedDate == null ? null : parsedDate.getTime(); @@ -757,18 +761,12 @@ private static Long convertToTimestamp(Context context, int timeInt, String date } } - private static final String TO_TIMESTAMP_FORMAT_PATTERN = "yyyyMMdd" + "HHmmss"; - - @Nullable - private static ThreadSafeDateFormatter toTimestampFormat; + private static final String DATE_AND_TIME_FORMAT_PATTERN = DATE_FORMAT_PATTERN + TIME_FORMAT_PATTERN; @NonNull - private static ThreadSafeDateFormatter getToTimestampFormat(Context context) { - if (toTimestampFormat == null) { - toTimestampFormat = new ThreadSafeDateFormatter(TO_TIMESTAMP_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); - toTimestampFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); - } + static DateFormat getNewDateAndTimeFormat(@NonNull TimeZone timeZone) { + final DateFormat toTimestampFormat = new SimpleDateFormat(DATE_AND_TIME_FORMAT_PATTERN, Locale.ENGLISH); + toTimestampFormat.setTimeZone(timeZone); return toTimestampFormat; } From 972fb9dde32d9cce0e6c16365bdf234243920b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sat, 8 Aug 2026 17:25:40 -0400 Subject: [PATCH 02/17] fix --- .../gtfs/GTFSScheduleTimestampsProvider.java | 6 +++--- .../commons/provider/gtfs/GTFSStatusProvider.java | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index ce28e624..afe6051a 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -63,8 +63,8 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro while (startsAt.getTimeInMillis() <= endsAtInMs) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); GTFSStatusProvider.alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.format(lookupStartAt); - lookupDayTime = timeFormat.format(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); + lookupDayTime = timeFormat.format(lookupStartAt.getTime()); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + GTFSStatusProvider.TWENTY_FOUR_HOURS); } else { // ELSE IF tomorrow or later DO @@ -84,7 +84,7 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro if (startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule && dayTimestamps.isEmpty()) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind - lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); // try 1 week before once dayTimestamps = GTFSStatusProvider.findScheduleList( provider, rds.getRoute().getId(), diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index daa29dd7..7d4c9e57 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -259,8 +259,8 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide while (dataRequests < maxDataRequests) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.format(lookupStartAt); - lookupDayTime = timeFormat.format(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); + lookupDayTime = timeFormat.format(lookupStartAt.getTime()); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO @@ -282,7 +282,7 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide if (dayTimestamps.isEmpty() && startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule ) { - lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); // try 1 week before once dayTimestamps = findScheduleList( provider, rds.getRoute().getId(), @@ -328,7 +328,7 @@ static void alignLookupStartTime( ) { if (lastServiceDate != null) { try { - while (Integer.parseInt(dateFormat.format(lookupStartAt)) > lastServiceDate) { + while (Integer.parseInt(dateFormat.format(lookupStartAt.getTime())) > lastServiceDate) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind } } catch (Exception e) { @@ -613,8 +613,8 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid while (dataRequests < maxDataRequests) { final Calendar lookupStartAt = TimeUtils.getNewCalendar(timeZone, startsAt.getTimeInMillis()); alignLookupStartTime(lastServiceDate, dateFormat, lookupStartAt, lastDepartureInMs); - lookupDayDate = dateFormat.format(lookupStartAt); - lookupDayTime = timeFormat.format(lookupStartAt); + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); + lookupDayTime = timeFormat.format(lookupStartAt.getTime()); if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO @@ -636,7 +636,7 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid && MIDNIGHT.equals(lookupDayTime) // not a partial schedule ) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind - lookupDayDate = dateFormat.format(lookupStartAt); // try 1 week before once + lookupDayDate = dateFormat.format(lookupStartAt.getTime()); // try 1 week before once dayFrequencies = findFrequencyList( provider, rds.getRoute().getId(), From 259dabf8ad9e0ea2c1ce679dd9312e1a1fb58eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sat, 8 Aug 2026 17:33:35 -0400 Subject: [PATCH 03/17] wip --- .../commons/provider/agency/AgencyUtils.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt index daf73d21..9ab70acb 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt @@ -1,10 +1,15 @@ package org.mtransit.android.commons.provider.agency import android.content.Context +import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.R import java.util.TimeZone -object AgencyUtils { +object AgencyUtils : MTLog.Loggable { + + private val LOG_TAG: String = AgencyUtils::class.java.simpleName + + override fun getLogTag() = LOG_TAG fun getAgencyShortName(context: Context) = context.getAgencyString( @@ -32,13 +37,14 @@ object AgencyUtils { .map { getString(it) } .firstOrNull { it.isNotBlank() } - private val _defaultTimeZoneId: String by lazy { TimeZone.getDefault().id } - @JvmStatic - fun getAgencyTimeZoneId(context: Context) = + fun getAgencyTimeZoneId(context: Context): String = context.getAgencyString( R.string.poi_agency_timezone, R.string.gtfs_rts_timezone, // do not change to avoid breaking compat w/ old modules R.string.bike_station_timezone, - ) ?: _defaultTimeZoneId + ) ?: run { + MTLog.w(LOG_TAG, "Impossible to read agency timezone! (using device timezone)") + TimeZone.getDefault().id + } } From 8612c902ac975d798417959eea9886a90fb7a0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sat, 8 Aug 2026 18:01:45 -0400 Subject: [PATCH 04/17] PR comments --- .../android/commons/data/Schedule.java | 37 ++++++++----------- .../commons/provider/agency/AgencyUtils.kt | 2 +- .../gtfs/GTFSScheduleTimestampsProvider.java | 5 ++- .../provider/gtfs/GTFSStatusProvider.java | 1 + 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index 666c5f52..65a71a76 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -455,8 +455,8 @@ public String getLogTag() { private int headsignType = Direction.HEADSIGN_TYPE_NONE; @Nullable private String headsignValue = null; - @Nullable - private String localTimeZoneId = null; + @NonNull + private final String localTimeZoneId; @Nullable private Boolean realTime = null; @Nullable @@ -474,7 +474,7 @@ public String getLogTag() { @VisibleForTesting public Timestamp(long departureT) { - this.departureInMs = departureT; + this(departureT, TimeZone.getDefault().getID()); } public Timestamp(long departureT, @NonNull TimeZone localTimeZone) { @@ -620,11 +620,7 @@ private String getNewHeading() { return Direction.getNewHeading(this.headsignType, this.headsignValue); } - private void setLocalTimeZoneId(@Nullable String localTimeZone) { - this.localTimeZoneId = localTimeZone; - } - - @Nullable + @NonNull public String getLocalTimeZoneId() { return localTimeZoneId; } @@ -742,7 +738,7 @@ public int hashCode() { result = 31 * result + Long.hashCode(originalDepartureDelayMs); result = 31 * result + headsignType; result = 31 * result + (headsignValue != null ? headsignValue.hashCode() : 0); - result = 31 * result + (localTimeZoneId != null ? localTimeZoneId.hashCode() : 0); + result = 31 * result + localTimeZoneId.hashCode(); result = 31 * result + (realTime != null ? realTime.hashCode() : 0); result = 31 * result + (oldSchedule != null ? oldSchedule.hashCode() : 0); result = 31 * result + (accessible != null ? accessible : 0); @@ -758,7 +754,7 @@ public int hashCode() { @NonNull @Override public String toString() { - StringBuilder sb = new StringBuilder(Timestamp.class.getSimpleName()); + final StringBuilder sb = new StringBuilder(Timestamp.class.getSimpleName()); sb.append('{'); sb.append("d=").append(Constants.DEBUG ? MTLog.formatDateTime(getDepartureT()) : getDepartureT()); if (this.originalDepartureDelayMs != 0L) { @@ -782,9 +778,7 @@ public String toString() { if (headsignValue != null) { sb.append(", hv:'").append(headsignValue).append('\''); } - if (localTimeZoneId != null) { - sb.append(", tz:'").append(localTimeZoneId).append('\''); - } + sb.append(", tz:'").append(localTimeZoneId).append('\''); if (realTime != null) { sb.append(", rt:").append(realTime); } @@ -809,7 +803,7 @@ public String toString() { private static final String JSON_STOP_SEQUENCE = "stop_seq"; private static final String JSON_HEADSIGN_TYPE = "ht"; private static final String JSON_HEADSIGN_VALUE = "hv"; - private static final String JSON_LOCAL_TIME_ZONE = "localTimeZone"; + private static final String JSON_LOCAL_TIME_ZONE_ID = "localTimeZone"; private static final String JSON_REAL_TIME = "rt"; private static final String JSON_OLD_SCHEDULE = "old"; private static final String JSON_ACCESSIBLE = "a11y"; @@ -819,7 +813,12 @@ public String toString() { static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { try { final long departureInMs = jTimestamp.getLong(JSON_DEPARTURE); - final Timestamp timestamp = new Timestamp(departureInMs); + String localTimeZoneId = jTimestamp.optString(JSON_LOCAL_TIME_ZONE_ID); + if (TextUtils.isEmpty(localTimeZoneId)) { + MTLog.w(LOG_TAG, "Timestamp missing timezone in JSON (using device TZ) '%s'!", jTimestamp); + localTimeZoneId = TimeZone.getDefault().getID(); + } + final Timestamp timestamp = new Timestamp(departureInMs, localTimeZoneId); final long originalDepartureDelayMs = jTimestamp.optLong(JSON_ORIGINAL_DEPARTURE_DELAY, 0L); if (originalDepartureDelayMs != 0L) { timestamp.setOriginalDepartureDelayMs(originalDepartureDelayMs); @@ -846,10 +845,6 @@ static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { timestamp.setHeadsign(headSignType, null); } } - final String localTimeZone = jTimestamp.optString(JSON_LOCAL_TIME_ZONE); - if (!TextUtils.isEmpty(localTimeZone)) { - timestamp.setLocalTimeZoneId(localTimeZone); - } if (jTimestamp.has(JSON_REAL_TIME)) { timestamp.setRealTime(jTimestamp.optBoolean(JSON_REAL_TIME, false)); } @@ -878,6 +873,7 @@ public JSONObject toJSON() { public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { final JSONObject jTimestamp = new JSONObject(); + jTimestamp.put(JSON_LOCAL_TIME_ZONE_ID, timestamp.localTimeZoneId); jTimestamp.put(JSON_DEPARTURE, timestamp.departureInMs); if (timestamp.originalDepartureDelayMs != 0L) { jTimestamp.put(JSON_ORIGINAL_DEPARTURE_DELAY, timestamp.originalDepartureDelayMs); @@ -902,9 +898,6 @@ public static JSONObject toJSON(@NonNull Timestamp timestamp) { jTimestamp.put(JSON_HEADSIGN_TYPE, timestamp.headsignType); } } - if (timestamp.localTimeZoneId != null) { - jTimestamp.put(JSON_LOCAL_TIME_ZONE, timestamp.localTimeZoneId); - } if (timestamp.realTime != null) { jTimestamp.put(JSON_REAL_TIME, timestamp.realTime); } diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt index 9ab70acb..b2efb15f 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt @@ -44,7 +44,7 @@ object AgencyUtils : MTLog.Loggable { R.string.gtfs_rts_timezone, // do not change to avoid breaking compat w/ old modules R.string.bike_station_timezone, ) ?: run { - MTLog.w(LOG_TAG, "Impossible to read agency timezone! (using device timezone)") + MTLog.w(LOG_TAG, "No agency timezone configured (using device timezone)!") TimeZone.getDefault().id } } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index afe6051a..761d50ad 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -81,8 +81,9 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro dateAndTimeFormat, agencyTimeZoneId ); - if (startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule - && dayTimestamps.isEmpty()) { + if (dayTimestamps.isEmpty() + && startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule + ) { lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind lookupDayDate = dateFormat.format(lookupStartAt.getTime()); // try 1 week before once dayTimestamps = GTFSStatusProvider.findScheduleList( diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index 7d4c9e57..452f61c0 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -282,6 +282,7 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide if (dayTimestamps.isEmpty() && startsAt.getTimeInMillis() > lookupStartAt.getTimeInMillis() // already looking at OLD schedule ) { + lookupStartAt.add(Calendar.DATE, -7); // look 1 week behind lookupDayDate = dateFormat.format(lookupStartAt.getTime()); // try 1 week before once dayTimestamps = findScheduleList( provider, From 7660b7023841235c8349362ce38cc0e13a0c3c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 09:17:09 -0400 Subject: [PATCH 05/17] wip --- .../android/commons/data/Schedule.java | 31 +++++++++++++------ .../android/commons/data/ScheduleExt.kt | 4 ++- .../provider/CleverDevicesProvider.java | 23 ++++++++------ .../provider/GTFSRealTimeProvider.java | 23 ++------------ .../commons/provider/NextBusProvider.java | 5 ++- .../commons/provider/OneBusAwayProvider.java | 1 + .../commons/provider/RTCQuebecProvider.java | 2 ++ .../provider/ReginaTransitProvider.java | 1 + .../commons/provider/StmInfoApiProvider.java | 1 + .../provider/WinnipegTransitProvider.java | 1 + .../commons/provider/agency/AgencyUtils.kt | 4 +++ .../provider/gtfs/GTFSRealTimeProviderExt.kt | 3 -- .../provider/gtfs/GTFSStatusProvider.java | 20 ++++++------ .../status/GTFSRealTimeTripUpdatesProvider.kt | 4 +-- src/main/res/values/gtfs_real_time_values.xml | 1 - 15 files changed, 67 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index 65a71a76..054605c7 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -13,6 +13,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.mtransit.android.commons.Constants; +import org.mtransit.android.commons.JSONUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.R; import org.mtransit.android.commons.StringUtils; @@ -51,10 +52,13 @@ public String getLogTag() { private boolean noPickup; + @Nullable + private final String localTimeZoneId; + @NonNull private final List frequencies = new ArrayList<>(); - public Schedule(@NonNull POIStatus status, long providerPrecisionInMs, boolean noPickup) { + public Schedule(@NonNull POIStatus status, long providerPrecisionInMs, boolean noPickup, @Nullable String localTimeZoneId) { this( status.getId(), status.getTargetUUID(), @@ -63,6 +67,7 @@ public Schedule(@NonNull POIStatus status, long providerPrecisionInMs, boolean n status.getReadFromSourceAtInMs(), providerPrecisionInMs, noPickup, + localTimeZoneId, status.getSourceLabel(), status.isNoData() ); @@ -76,9 +81,10 @@ public Schedule( long readFromSourceAtInMs, long providerPrecisionInMs, boolean noPickup, + @Nullable String localTimeZoneId, @Nullable String sourceLabel ) { - this(id, targetUUID, lastUpdateInMs, maxValidityInMs, readFromSourceAtInMs, providerPrecisionInMs, noPickup, sourceLabel, false); + this(id, targetUUID, lastUpdateInMs, maxValidityInMs, readFromSourceAtInMs, providerPrecisionInMs, noPickup, localTimeZoneId, sourceLabel, false); } public Schedule( @@ -89,12 +95,14 @@ public Schedule( long readFromSourceAtInMs, long providerPrecisionInMs, boolean noPickup, + @Nullable String localTimeZoneId, @Nullable String sourceLabel, boolean noData ) { super(id, targetUUID, POI.ITEM_STATUS_TYPE_SCHEDULE, lastUpdateInMs, maxValidityInMs, readFromSourceAtInMs, sourceLabel, noData); - this.noPickup = noPickup; this.providerPrecisionInMs = providerPrecisionInMs; + this.noPickup = noPickup; + this.localTimeZoneId = localTimeZoneId; resetTimestampsUntilInMs(); } @@ -102,6 +110,11 @@ public boolean isNoPickup() { return noPickup; } + @Nullable + public String getLocalTimeZoneId() { + return localTimeZoneId; + } + public long getProviderPrecisionInMs() { return providerPrecisionInMs; } @@ -141,7 +154,8 @@ private static Schedule fromExtraJSON(@NonNull POIStatus status, @NonNull JSONOb try { final long providerPrecisionInMs = extrasJSON.getInt(JSON_PROVIDER_PRECISION_IN_MS); final boolean noPickup = extrasJSON.optBoolean(JSON_IS_NO_PICKUP, false); - final Schedule schedule = new Schedule(status, providerPrecisionInMs, noPickup); + final String localTimeZoneId = JSONUtils.optString(extrasJSON, JSON_LOCAL_TIME_ZONE_ID); + final Schedule schedule = new Schedule(status, providerPrecisionInMs, noPickup, localTimeZoneId); final JSONArray jTimestamps = extrasJSON.getJSONArray(JSON_TIMESTAMPS); for (int i = 0; i < jTimestamps.length(); i++) { final JSONObject jTimestamp = jTimestamps.getJSONObject(i); @@ -165,6 +179,7 @@ private static Schedule fromExtraJSON(@NonNull POIStatus status, @NonNull JSONOb private static final String JSON_IS_NO_PICKUP = "decentOnly"; // do NOT change JSON key string value! private static final String JSON_TIMESTAMPS = "timestamps"; private static final String JSON_FREQUENCIES = "frequencies"; + private static final String JSON_LOCAL_TIME_ZONE_ID = "tz"; @Nullable @Override @@ -173,6 +188,9 @@ public JSONObject getExtrasJSON() { JSONObject json = new JSONObject(); json.put(JSON_PROVIDER_PRECISION_IN_MS, this.providerPrecisionInMs); json.put(JSON_IS_NO_PICKUP, this.noPickup); + if (this.localTimeZoneId != null) { + json.put(JSON_LOCAL_TIME_ZONE_ID, this.localTimeZoneId); + } JSONArray jTimestamps = new JSONArray(); for (Timestamp timestamp : this.timestamps) { jTimestamps.put(timestamp.toJSON()); @@ -625,11 +643,6 @@ public String getLocalTimeZoneId() { return localTimeZoneId; } - @Deprecated - public boolean hasLocalTimeZoneId() { - return !TextUtils.isEmpty(this.localTimeZoneId); - } - public void setRealTime(@Nullable Boolean realTime) { this.realTime = realTime; } diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 958497cd..7ba63718 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -21,7 +21,8 @@ fun makeSchedule( providerPrecisionInMs: Long, isNoPickup: Boolean = false, sourceLabel: String? = null, - noData: Boolean = false + noData: Boolean = false, + localTimeZoneId: String? = null, ) = Schedule( id, targetUUID, @@ -30,6 +31,7 @@ fun makeSchedule( readFromSourceAtInMs, providerPrecisionInMs, isNoPickup, + localTimeZoneId, sourceLabel, noData, ) diff --git a/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java b/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java index dbfb9cd5..6023333a 100644 --- a/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java @@ -276,7 +276,9 @@ private void loadRealTimeStatusFromWWW(@NonNull RouteDirectionStop rds) { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); - CleverDevicesPredictionsDataHandler handler = new CleverDevicesPredictionsDataHandler(this, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context), sourceLabel, rds); + CleverDevicesPredictionsDataHandler handler = new CleverDevicesPredictionsDataHandler( + this, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context), sourceLabel, rds + ); xr.setContentHandler(handler); xr.parse(new InputSource(httpUrlConnection.getInputStream())); Collection statuses = handler.getStatuses(); @@ -447,7 +449,7 @@ public String getLogTag() { private final CleverDevicesProvider provider; private final long lastUpdateInMs; @NonNull - private final String timeZoneId; + private final String localTimeZoneId; @Nullable private final String sourceLabel; @NonNull @@ -465,14 +467,16 @@ public String getLogTag() { @NonNull private final HashSet statuses = new HashSet<>(); - CleverDevicesPredictionsDataHandler(@NonNull CleverDevicesProvider provider, - long lastUpdateInMs, - @NonNull String timeZoneId, - @Nullable String sourceLabel, - @NonNull RouteDirectionStop rds) { + CleverDevicesPredictionsDataHandler( + @NonNull CleverDevicesProvider provider, + long lastUpdateInMs, + @NonNull String localTimeZoneId, + @Nullable String sourceLabel, + @NonNull RouteDirectionStop rds + ) { this.provider = provider; this.lastUpdateInMs = lastUpdateInMs; - this.timeZoneId = timeZoneId; + this.localTimeZoneId = localTimeZoneId; this.sourceLabel = sourceLabel; this.rds = rds; } @@ -543,7 +547,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc return; } long t = TimeUtils.timeToTheMinuteMillis(this.lastUpdateInMs) + TimeUnit.MINUTES.toMillis(minutes); - Schedule.Timestamp timestamp = new Schedule.Timestamp(t, this.timeZoneId); + Schedule.Timestamp timestamp = new Schedule.Timestamp(t, this.localTimeZoneId); if (!TextUtils.isEmpty(this.currentFd)) { timestamp.setHeadsign(Direction.HEADSIGN_TYPE_STRING, cleanTripHeadsign(this.provider.requireContextCompat(), this.currentFd.toString().trim(), rds)); } @@ -564,6 +568,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc this.lastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + this.localTimeZoneId, this.sourceLabel ); newSchedule.setTimestampsAndSort(this.currentTimestamps); diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index 6804093a..f994c903 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -576,20 +576,6 @@ private static String getAGENCY_TIME_AM_PM_FORMAT(@NonNull Context context) { return agencyTimeAmPmFormat; } - @Nullable - private static String agencyTimeZoneId = null; - - /** - * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. - */ - @NonNull - public static String getAGENCY_TIME_ZONE_ID(@NonNull Context context) { - if (agencyTimeZoneId == null) { - agencyTimeZoneId = context.getResources().getString(R.string.gtfs_real_time_agency_time_zone); - } - return agencyTimeZoneId; - } - @Override public long getStatusMaxValidityInMs() { return GTFSRealTimeTripUpdatesProvider.adaptForCachedAPI(StatusProviderContract.super.getStatusMaxValidityInMs(), getContext()); @@ -1244,13 +1230,8 @@ private ThreadSafeDateFormatter getTimeParser(@NonNull Context context) { formatter += StringUtils.SPACE_STRING + getAGENCY_TIME_AM_PM_FORMAT(context); } timeParser = new ThreadSafeDateFormatter(formatter, Locale.ENGLISH); - String agencyTimeZoneId = getAGENCY_TIME_ZONE_ID(context); - if (TextUtils.isEmpty(agencyTimeZoneId)) { - agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); - } - if (!TextUtils.isEmpty(agencyTimeZoneId)) { - timeParser.setTimeZone(TimeZone.getTimeZone(agencyTimeZoneId)); - } + final String agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); + timeParser.setTimeZone(TimeZone.getTimeZone(agencyTimeZoneId)); } catch (Exception e) { MTLog.w(ALERTS_LOG_TAG, e, "Error while initializing time formatter!"); timeParser = null; diff --git a/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java b/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java index 992dc5e5..2fbf7748 100644 --- a/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java @@ -1072,7 +1072,9 @@ private void loadPredictionsFromWWW(@NonNull Context context, @NonNull RouteDire final SAXParserFactory spf = SAXParserFactory.newInstance(); final SAXParser sp = spf.newSAXParser(); final XMLReader xr = sp.getXMLReader(); - final NextBusPredictionsDataHandler handler = new NextBusPredictionsDataHandler(this, sourceLabel, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context)); + final NextBusPredictionsDataHandler handler = new NextBusPredictionsDataHandler( + this, sourceLabel, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context) + ); xr.setContentHandler(handler); xr.parse(new InputSource(response.body().byteStream())); final Collection statuses = handler.getStatuses(); @@ -1386,6 +1388,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc this.lastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + this.localTimeZoneId, this.sourceLabel ); } diff --git a/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java b/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java index 2f41b960..ed1c0dd2 100644 --- a/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java @@ -375,6 +375,7 @@ private Collection parseAgencyJSON(@NonNull Context context, @Nullabl newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + localTimeZoneId, sourceLabel ); for (int l = 0; l < jArrivalsAndDepartures.length(); l++) { diff --git a/src/main/java/org/mtransit/android/commons/provider/RTCQuebecProvider.java b/src/main/java/org/mtransit/android/commons/provider/RTCQuebecProvider.java index ed0261ce..cef2d5ef 100644 --- a/src/main/java/org/mtransit/android/commons/provider/RTCQuebecProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/RTCQuebecProvider.java @@ -869,6 +869,7 @@ private Collection parseAgencyJSONArretParcoursHoraires( newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, jArretParcours.isDescenteSeulement(), + QUEBEC_CITY_TZ.getID(), sourceLabel, true // NO DATA // keep = no service today ) @@ -883,6 +884,7 @@ private Collection parseAgencyJSONArretParcoursHoraires( newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, jArretParcours.isDescenteSeulement(), + QUEBEC_CITY_TZ.getID(), sourceLabel ); for (int r = 0; r < jHoraires.size(); r++) { diff --git a/src/main/java/org/mtransit/android/commons/provider/ReginaTransitProvider.java b/src/main/java/org/mtransit/android/commons/provider/ReginaTransitProvider.java index e2306a15..635c9b22 100644 --- a/src/main/java/org/mtransit/android/commons/provider/ReginaTransitProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/ReginaTransitProvider.java @@ -262,6 +262,7 @@ private Collection parseAgencyJSON(String jsonString, RouteDirectionS newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + REGINA_TZ.getID(), sourceLabel ); Calendar beginningOfTodayCal = Calendar.getInstance(REGINA_TZ); diff --git a/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java b/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java index 468f97fa..5980f66a 100644 --- a/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java @@ -1379,6 +1379,7 @@ protected ArrayList parseAgencyJSONArrivalsStatuses( newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + MONTREAL_TZ.getID(), sourceLabel ); for (int r = 0; r < jResults.size(); r++) { diff --git a/src/main/java/org/mtransit/android/commons/provider/WinnipegTransitProvider.java b/src/main/java/org/mtransit/android/commons/provider/WinnipegTransitProvider.java index fe4d08bc..071c42a2 100644 --- a/src/main/java/org/mtransit/android/commons/provider/WinnipegTransitProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/WinnipegTransitProvider.java @@ -402,6 +402,7 @@ private Schedule parseAgencySchedule(@NonNull Context context, @NonNull RouteDir newLastUpdateInMs, PROVIDER_PRECISION_IN_MS, false, + WINNIPEG_TZ.getID(), sourceLabel ); String directionIdS = String.valueOf(rds.getDirection().getId()); diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt index b2efb15f..985af1b5 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt @@ -1,6 +1,7 @@ package org.mtransit.android.commons.provider.agency import android.content.Context +import org.mtransit.android.commons.BuildConfig import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.R import java.util.TimeZone @@ -44,6 +45,9 @@ object AgencyUtils : MTLog.Loggable { R.string.gtfs_rts_timezone, // do not change to avoid breaking compat w/ old modules R.string.bike_station_timezone, ) ?: run { + if (BuildConfig.DEBUG) { + throw RuntimeException("No agency timezone configured!") + } MTLog.w(LOG_TAG, "No agency timezone configured (using device timezone)!") TimeZone.getDefault().id } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt index fea5bdc1..a4905b98 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt @@ -11,7 +11,6 @@ import org.mtransit.android.commons.data.Stop import org.mtransit.android.commons.data.toRouteDirection import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.MT_HASH_SECRET_AND_DATE -import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_TIME_ZONE_ID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_HEADER_NAMES import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_HEADER_VALUES import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_TOKEN @@ -39,8 +38,6 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTU val Int.isValidDirection get() = this in 0..1 val GTFSRealTimeProvider.ignoreDirection get() = isIGNORE_DIRECTION(requireContextCompat()) val GTFSRealTimeProvider.targetAuthority get() = getTARGET_AUTHORITY(requireContextCompat()) -val GTFSRealTimeProvider.timeZoneId get() = getAGENCY_TIME_ZONE_ID(requireContextCompat()) -val GTFSRealTimeProvider.optTimeZoneId get() = timeZoneId.takeIf { it.isNotBlank() } fun GTFSRealTimeProvider.parseAgencyId(es: GEntitySelector) = es.optAgencyIdNotEmpty?.let { parseAgencyId(it) } fun GTFSRealTimeProvider.parseAgencyId(gAgencyId: String) = gAgencyId.originalIdToId(agencyIdCleanupPattern) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index 452f61c0..06939368 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -148,6 +148,8 @@ public static POIStatus getNewStatus(@NonNull GTFSProvider provider, @NonNull St return null; } final ScheduleStatusFilter scheduleStatusFilter = (ScheduleStatusFilter) statusFilter; + final Context context = provider.requireContextCompat(); + final String agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); final Schedule schedule = new Schedule( null, scheduleStatusFilter.getTargetUUID(), @@ -156,13 +158,14 @@ public static POIStatus getNewStatus(@NonNull GTFSProvider provider, @NonNull St PROVIDER_READ_FROM_SOURCE_AT_IN_MS, PROVIDER_PRECISION_IN_MS, scheduleStatusFilter.getRouteDirectionStop().isNoPickup(), - GTFSProvider.getSOURCE_LABEL(provider.requireContextCompat()) + agencyTimeZoneId, + GTFSProvider.getSOURCE_LABEL(context) ); - if (isSCHEDULE_AVAILABLE(provider.requireContextCompat())) { - schedule.setTimestampsAndSort(findTimestamps(provider, scheduleStatusFilter)); + if (isSCHEDULE_AVAILABLE(context)) { + schedule.setTimestampsAndSort(findTimestamps(provider, scheduleStatusFilter, agencyTimeZoneId)); } - if (isFREQUENCY_AVAILABLE(provider.requireContextCompat())) { - schedule.setFrequenciesAndSort(findFrequencies(provider, scheduleStatusFilter)); + if (isFREQUENCY_AVAILABLE(context)) { + schedule.setFrequenciesAndSort(findFrequencies(provider, scheduleStatusFilter, agencyTimeZoneId)); } return schedule; } @@ -223,7 +226,7 @@ private static String getROUTE_FREQUENCY_RAW_FILE_FORMAT(@NonNull Context contex private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_COUNT = 5; @NonNull - private static ArrayList findTimestamps(@NonNull GTFSProvider provider, @NonNull ScheduleStatusFilter filter) { + private static ArrayList findTimestamps(@NonNull GTFSProvider provider, @NonNull ScheduleStatusFilter filter, @NonNull String agencyTimeZoneId) { ArrayList allTimestamps = new ArrayList<>(); final RouteDirectionStop rds = filter.getRouteDirectionStop(); final int maxDataRequests = filter.getMaxDataRequestsOrDefault(); @@ -233,7 +236,6 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide final long timestamp = filter.getTimestampOrDefault(); // NOW final long minTimestampCoveredIntMs = timestamp + minDurationCoveredInMs; final Context context = provider.requireContextCompat(); - final String agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); final TimeZone timeZone = TimeZone.getTimeZone(agencyTimeZoneId); final DateFormat dateFormat = getNewDateFormat(timeZone); final DateFormat timeFormat = getNewTimeFormat(timeZone); @@ -591,7 +593,7 @@ protected static Set filterServiceIdOrInts(@NonNull Set findFrequencies(@NonNull GTFSProvider provider, @NonNull ScheduleStatusFilter filter) { + private static ArrayList findFrequencies(@NonNull GTFSProvider provider, @NonNull ScheduleStatusFilter filter, @NonNull String agencyTimeZoneId) { final ArrayList allFrequencies = new ArrayList<>(); final RouteDirectionStop rds = filter.getRouteDirectionStop(); final int maxDataRequests = filter.getMaxDataRequestsOrDefault(); @@ -599,7 +601,7 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid final long timestamp = filter.getTimestampOrDefault(); // NOW final long minTimestampCovered = timestamp + minDurationCoveredInMs; final Context context = provider.requireContextCompat(); - final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getAgencyTimeZoneId(context)); + final TimeZone timeZone = TimeZone.getTimeZone(agencyTimeZoneId); final DateFormat dateFormat = getNewDateFormat(timeZone); final DateFormat timeFormat = getNewTimeFormat(timeZone); final DateFormat dateAndTimeFormat = getNewDateAndTimeFormat(timeZone); diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index 6833aa60..3c89e82c 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -43,7 +43,6 @@ import org.mtransit.android.commons.provider.gtfs.getRDSSchedule import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.ignoreDirection import org.mtransit.android.commons.provider.gtfs.makeRequest -import org.mtransit.android.commons.provider.gtfs.optTimeZoneId import org.mtransit.android.commons.provider.gtfs.parseRouteId import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.gtfs.storage @@ -208,8 +207,7 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { val rdSchedules: Collection by lazy { context.getRDSSchedule(targetAuthority, sortedRDS, filter.isIncludeCancelledTimestampsOrDefault) } - val agencyTimeZoneId = this.optTimeZoneId - ?: AgencyUtils.getAgencyTimeZoneId(context) + val agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context) val agencyTimeZone = runCatching { KtTimeZone.of(agencyTimeZoneId) }.getOrElse { e -> MTLog.w(LOG_TAG, e, "makeCachedStatusFromAgencyData() > error getting timezone from '$agencyTimeZoneId'!") KtTimeZone.currentSystemDefault() diff --git a/src/main/res/values/gtfs_real_time_values.xml b/src/main/res/values/gtfs_real_time_values.xml index f7c867e1..d31223f6 100755 --- a/src/main/res/values/gtfs_real_time_values.xml +++ b/src/main/res/values/gtfs_real_time_values.xml @@ -23,5 +23,4 @@ - @string/gtfs_rts_timezone \ No newline at end of file From 3dc5f8a8a3429516464b89993e0e9d697c958830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 10:02:30 -0400 Subject: [PATCH 06/17] wip --- .../android/commons/data/Schedule.java | 37 +++++++++++-------- .../android/commons/data/ScheduleExt.kt | 2 +- .../commons/data/ScheduleTimestamps.java | 30 ++++++++++++--- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index 054605c7..77637965 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -12,6 +12,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.mtransit.android.commons.BuildConfig; import org.mtransit.android.commons.Constants; import org.mtransit.android.commons.JSONUtils; import org.mtransit.android.commons.MTLog; @@ -154,14 +155,21 @@ private static Schedule fromExtraJSON(@NonNull POIStatus status, @NonNull JSONOb try { final long providerPrecisionInMs = extrasJSON.getInt(JSON_PROVIDER_PRECISION_IN_MS); final boolean noPickup = extrasJSON.optBoolean(JSON_IS_NO_PICKUP, false); - final String localTimeZoneId = JSONUtils.optString(extrasJSON, JSON_LOCAL_TIME_ZONE_ID); - final Schedule schedule = new Schedule(status, providerPrecisionInMs, noPickup, localTimeZoneId); + String localTimeZoneId = JSONUtils.optString(extrasJSON, JSON_LOCAL_TIME_ZONE_ID); + final ArrayList timestamps = new ArrayList<>(); final JSONArray jTimestamps = extrasJSON.getJSONArray(JSON_TIMESTAMPS); for (int i = 0; i < jTimestamps.length(); i++) { final JSONObject jTimestamp = jTimestamps.getJSONObject(i); - schedule.addTimestampWithoutSort(Timestamp.parseJSON(jTimestamp)); + final Timestamp timestamp = Timestamp.parseJSON(jTimestamp); + if (timestamp == null) continue; + if (localTimeZoneId == null) { + //noinspection deprecation + localTimeZoneId = timestamp.getLocalTimeZoneId(); + } + timestamps.add(timestamp); } - schedule.sortTimestamps(); + final Schedule schedule = new Schedule(status, providerPrecisionInMs, noPickup, localTimeZoneId); + schedule.setTimestampsAndSort(timestamps); final JSONArray jFrequencies = extrasJSON.getJSONArray(JSON_FREQUENCIES); for (int i = 0; i < jFrequencies.length(); i++) { final JSONObject jFrequency = jFrequencies.getJSONObject(i); @@ -281,17 +289,6 @@ public int getTimestampsCount() { return this.timestamps.size(); } - @Nullable - public TimeZone getTimeZone() { - for (Timestamp timestamp : this.timestamps) { - final String localTimeZoneId = timestamp.getLocalTimeZoneId(); - if (localTimeZoneId != null) { - return TimeZone.getTimeZone(localTimeZoneId); - } - } - return null; - } - protected static final long MIN_UI_PRECISION_IN_MS = TimeUnit.MINUTES.toMillis(1L); protected long getUIProviderPrecisionInMs() { @@ -474,7 +471,7 @@ public String getLogTag() { @Nullable private String headsignValue = null; @NonNull - private final String localTimeZoneId; + private final String localTimeZoneId; // TODO remove once migrated fully to Schedule TZ @Nullable private Boolean realTime = null; @Nullable @@ -638,6 +635,11 @@ private String getNewHeading() { return Direction.getNewHeading(this.headsignType, this.headsignValue); } + /** + * @deprecated use schedule TZ if available + */ + @SuppressWarnings("DeprecatedIsStillUsed") // migrating to Schedule TZ + @Deprecated @NonNull public String getLocalTimeZoneId() { return localTimeZoneId; @@ -828,6 +830,9 @@ static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { final long departureInMs = jTimestamp.getLong(JSON_DEPARTURE); String localTimeZoneId = jTimestamp.optString(JSON_LOCAL_TIME_ZONE_ID); if (TextUtils.isEmpty(localTimeZoneId)) { + if (BuildConfig.DEBUG) { + throw new RuntimeException("Timestamp missing timezone in JSON!"); + } MTLog.w(LOG_TAG, "Timestamp missing timezone in JSON (using device TZ) '%s'!", jTimestamp); localTimeZoneId = TimeZone.getDefault().getID(); } diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 7ba63718..a68d4457 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -266,7 +266,7 @@ fun Schedule.Timestamp.toStringShort() = buildString { if (tripId != null) { append("[tId:").append(tripId).append("]") } - localTimeZoneId?.let { + localTimeZoneId.let { append("[tz:").append(it).append("]") } if (isRealTime) { diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java index 8ca5380f..f7744740 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java @@ -9,6 +9,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.mtransit.android.commons.JSONUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.provider.scheduletimestamp.ScheduleTimestampsProviderContract; import org.mtransit.commons.CollectionUtils; @@ -29,6 +30,8 @@ public String getLogTag() { @NonNull private List timestamps = new ArrayList<>(); @Nullable + private String localTimeZoneId; + @Nullable private String sourceLabel = null; @NonNull private final String targetUUID; @@ -74,6 +77,15 @@ public String getSourceLabel() { return sourceLabel; } + public void setLocalTimeZoneId(@Nullable String localTimeZoneId) { + this.localTimeZoneId = localTimeZoneId; + } + + @Nullable + public String getLocalTimeZoneId() { + return localTimeZoneId; + } + @Nullable public static ScheduleTimestamps fromCursor(@NonNull Cursor cursor) { final String targetUUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_TARGET_UUID)); @@ -88,9 +100,7 @@ public static ScheduleTimestamps fromCursor(@NonNull Cursor cursor) { private static ScheduleTimestamps fromExtraJSONString(ScheduleTimestamps scheduleTimestamps, String extrasJSONString) { try { final JSONObject json = extrasJSONString == null ? null : new JSONObject(extrasJSONString); - if (json == null) { - return null; - } + if (json == null) return null; return fromExtraJSON(scheduleTimestamps, json); } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while retrieving extras information from cursor."); @@ -99,16 +109,25 @@ private static ScheduleTimestamps fromExtraJSONString(ScheduleTimestamps schedul } private static final String JSON_SOURCE_LABEL = "sourceLabel"; + private static final String JSON_LOCAL_TIME_ZONE_ID = "tz"; private static final String JSON_TIMESTAMPS = "timestamps"; private static ScheduleTimestamps fromExtraJSON(ScheduleTimestamps scheduleTimestamps, JSONObject extrasJSON) { try { - scheduleTimestamps.setSourceLabel(extrasJSON.optString(JSON_SOURCE_LABEL, null)); + scheduleTimestamps.setSourceLabel(JSONUtils.optString(extrasJSON, JSON_SOURCE_LABEL)); + String localTimeZoneId = JSONUtils.optString(extrasJSON, JSON_LOCAL_TIME_ZONE_ID); final JSONArray jTimestamps = extrasJSON.getJSONArray(JSON_TIMESTAMPS); for (int i = 0; i < jTimestamps.length(); i++) { final JSONObject jTimestamp = jTimestamps.getJSONObject(i); - scheduleTimestamps.addTimestampWithoutSort(Schedule.Timestamp.parseJSON(jTimestamp)); + final Schedule.Timestamp newTimestamp = Schedule.Timestamp.parseJSON(jTimestamp); + if (newTimestamp == null) continue; + if (localTimeZoneId == null) { + //noinspection deprecation + localTimeZoneId = newTimestamp.getLocalTimeZoneId(); + } + scheduleTimestamps.addTimestampWithoutSort(newTimestamp); } + scheduleTimestamps.setLocalTimeZoneId(localTimeZoneId); scheduleTimestamps.sortTimestamps(); return scheduleTimestamps; } catch (JSONException jsone) { @@ -146,6 +165,7 @@ public JSONObject getExtrasJSON() { try { final JSONObject json = new JSONObject(); json.put(JSON_SOURCE_LABEL, this.sourceLabel); + json.put(JSON_LOCAL_TIME_ZONE_ID, this.localTimeZoneId); final JSONArray jTimestamps = new JSONArray(); for (Schedule.Timestamp timestamp : this.timestamps) { jTimestamps.put(timestamp.toJSON()); From f83383ca7a1f455b3e5afc3b1be6d1115664a24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 10:13:26 -0400 Subject: [PATCH 07/17] wip --- src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index a68d4457..67f45554 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -266,6 +266,7 @@ fun Schedule.Timestamp.toStringShort() = buildString { if (tripId != null) { append("[tId:").append(tripId).append("]") } + @Suppress("DEPRECATION") // migrating localTimeZoneId.let { append("[tz:").append(it).append("]") } From b1a6a37ca9542ddfc82f03ead701b5ea7af09fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 10:26:40 -0400 Subject: [PATCH 08/17] wip --- .../java/org/mtransit/android/commons/data/ScheduleExt.kt | 5 ++++- .../provider/status/GTFSRealTimeTripUpdatesProvider.kt | 1 + .../provider/status/GTFSRealTimeTripUpdatesProviderTests.kt | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 67f45554..e43386d3 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -20,9 +20,9 @@ fun makeSchedule( readFromSourceAtInMs: Long, providerPrecisionInMs: Long, isNoPickup: Boolean = false, + localTimeZoneId: String?, sourceLabel: String? = null, noData: Boolean = false, - localTimeZoneId: String? = null, ) = Schedule( id, targetUUID, @@ -41,6 +41,7 @@ fun RouteDirectionStop.makeSchedule( maxValidityInMs: Long, readFromSourceAtInMs: Long, providerPrecisionInMs: Long, + localTimeZoneId: String, sourceLabel: String, noData: Boolean, ) = makeSchedule( @@ -49,6 +50,7 @@ fun RouteDirectionStop.makeSchedule( maxValidityInMs = maxValidityInMs, readFromSourceAtInMs = readFromSourceAtInMs, providerPrecisionInMs = providerPrecisionInMs, + localTimeZoneId = localTimeZoneId, sourceLabel = sourceLabel, noData = noData ).apply { @@ -63,6 +65,7 @@ fun Schedule.toNoData() = makeSchedule( readFromSourceAtInMs = readFromSourceAtInMs, providerPrecisionInMs = providerPrecisionInMs, isNoPickup = isNoPickup, + localTimeZoneId = localTimeZoneId, sourceLabel = sourceLabel, noData = true // NO DATA ) diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index 3c89e82c..9f206362 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -234,6 +234,7 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { maxValidityInMs = statusMaxValidityInMs, readFromSourceAtInMs = feedReadFromSourceMs, providerPrecisionInMs = PROVIDER_PRECISION_IN_MS, + localTimeZoneId = agencyTimeZoneId, sourceLabel = sourceLabel, noData = true, // NO DATA ).let { noDataStatus -> diff --git a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt index 5d0180df..8dc52a81 100644 --- a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt +++ b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt @@ -1269,6 +1269,7 @@ class GTFSRealTimeTripUpdatesProviderTests { maxValidityInMs = nowInMs, readFromSourceAtInMs = nowInMs, providerPrecisionInMs = GTFSStatusProvider.PROVIDER_PRECISION_IN_MS, + localTimeZoneId = LOCAL_TZ_ID, ).apply { setTimestampsAndSort(timestamps) } From bd30985c8018653072d4b8eb1e7ea95819d17145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 10:37:26 -0400 Subject: [PATCH 09/17] fix --- .../mtransit/android/commons/provider/agency/AgencyUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt index 985af1b5..b05cbef3 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt @@ -46,9 +46,9 @@ object AgencyUtils : MTLog.Loggable { R.string.bike_station_timezone, ) ?: run { if (BuildConfig.DEBUG) { - throw RuntimeException("No agency timezone configured!") + throw IllegalStateException("No agency timezone configured!") } - MTLog.w(LOG_TAG, "No agency timezone configured (using device timezone)!") + MTLog.w(LOG_TAG, "No agency timezone configured (using device TZ)!") TimeZone.getDefault().id } } From 343f86ba9cc1faaef62bfb97dfa10b5375a247ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 11:39:03 -0400 Subject: [PATCH 10/17] fix --- src/main/java/org/mtransit/android/commons/data/Schedule.java | 2 +- src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt | 2 ++ .../org/mtransit/android/commons/data/ScheduleTimestamps.java | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index 77637965..8f235d14 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -163,7 +163,7 @@ private static Schedule fromExtraJSON(@NonNull POIStatus status, @NonNull JSONOb final Timestamp timestamp = Timestamp.parseJSON(jTimestamp); if (timestamp == null) continue; if (localTimeZoneId == null) { - //noinspection deprecation + //noinspection deprecation, DeprecatedCall localTimeZoneId = timestamp.getLocalTimeZoneId(); } timestamps.add(timestamp); diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index e43386d3..0061bdda 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -1,5 +1,6 @@ package org.mtransit.android.commons.data +import android.annotation.SuppressLint import org.mtransit.android.commons.Constants import org.mtransit.android.commons.floorBy import org.mtransit.android.commons.millisToInstant @@ -269,6 +270,7 @@ fun Schedule.Timestamp.toStringShort() = buildString { if (tripId != null) { append("[tId:").append(tripId).append("]") } + @SuppressLint("DeprecatedCall") @Suppress("DEPRECATION") // migrating localTimeZoneId.let { append("[tz:").append(it).append("]") diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java index f7744740..0af44d4f 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java @@ -122,7 +122,7 @@ private static ScheduleTimestamps fromExtraJSON(ScheduleTimestamps scheduleTimes final Schedule.Timestamp newTimestamp = Schedule.Timestamp.parseJSON(jTimestamp); if (newTimestamp == null) continue; if (localTimeZoneId == null) { - //noinspection deprecation + //noinspection deprecation, DeprecatedCall localTimeZoneId = newTimestamp.getLocalTimeZoneId(); } scheduleTimestamps.addTimestampWithoutSort(newTimestamp); From 559d02a34f0dd4cacfc8d0382ec6303bb6b506eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Sun, 9 Aug 2026 16:40:22 -0400 Subject: [PATCH 11/17] fix --- .../android/commons/data/Schedule.java | 35 +++++++++---------- .../android/commons/data/ScheduleExt.kt | 6 ++-- .../commons/data/ScheduleTimestamps.java | 3 +- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index 8f235d14..30d8d5db 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -163,7 +163,7 @@ private static Schedule fromExtraJSON(@NonNull POIStatus status, @NonNull JSONOb final Timestamp timestamp = Timestamp.parseJSON(jTimestamp); if (timestamp == null) continue; if (localTimeZoneId == null) { - //noinspection deprecation, DeprecatedCall + //noinspection DiscouragedApi localTimeZoneId = timestamp.getLocalTimeZoneId(); } timestamps.add(timestamp); @@ -470,7 +470,7 @@ public String getLogTag() { private int headsignType = Direction.HEADSIGN_TYPE_NONE; @Nullable private String headsignValue = null; - @NonNull + @Nullable private final String localTimeZoneId; // TODO remove once migrated fully to Schedule TZ @Nullable private Boolean realTime = null; @@ -489,14 +489,14 @@ public String getLogTag() { @VisibleForTesting public Timestamp(long departureT) { - this(departureT, TimeZone.getDefault().getID()); + this(departureT, (String) null); } public Timestamp(long departureT, @NonNull TimeZone localTimeZone) { this(departureT, localTimeZone.getID()); } - public Timestamp(long departureT, @NonNull String localTimeZoneId) { + public Timestamp(long departureT, @Nullable String localTimeZoneId) { this.departureInMs = departureT; this.localTimeZoneId = localTimeZoneId; } @@ -635,12 +635,8 @@ private String getNewHeading() { return Direction.getNewHeading(this.headsignType, this.headsignValue); } - /** - * @deprecated use schedule TZ if available - */ - @SuppressWarnings("DeprecatedIsStillUsed") // migrating to Schedule TZ - @Deprecated - @NonNull + @Discouraged(message = "should use parent Schedule local time zone") + @Nullable public String getLocalTimeZoneId() { return localTimeZoneId; } @@ -753,7 +749,7 @@ public int hashCode() { result = 31 * result + Long.hashCode(originalDepartureDelayMs); result = 31 * result + headsignType; result = 31 * result + (headsignValue != null ? headsignValue.hashCode() : 0); - result = 31 * result + localTimeZoneId.hashCode(); + result = 31 * result + (localTimeZoneId != null ? localTimeZoneId.hashCode() : 0); result = 31 * result + (realTime != null ? realTime.hashCode() : 0); result = 31 * result + (oldSchedule != null ? oldSchedule.hashCode() : 0); result = 31 * result + (accessible != null ? accessible : 0); @@ -793,7 +789,9 @@ public String toString() { if (headsignValue != null) { sb.append(", hv:'").append(headsignValue).append('\''); } - sb.append(", tz:'").append(localTimeZoneId).append('\''); + if (localTimeZoneId != null) { + sb.append(", tz:'").append(localTimeZoneId).append('\''); + } if (realTime != null) { sb.append(", rt:").append(realTime); } @@ -828,13 +826,12 @@ public String toString() { static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { try { final long departureInMs = jTimestamp.getLong(JSON_DEPARTURE); - String localTimeZoneId = jTimestamp.optString(JSON_LOCAL_TIME_ZONE_ID); - if (TextUtils.isEmpty(localTimeZoneId)) { + final String localTimeZoneId = JSONUtils.optString(jTimestamp, JSON_LOCAL_TIME_ZONE_ID); + if (localTimeZoneId == null) { if (BuildConfig.DEBUG) { - throw new RuntimeException("Timestamp missing timezone in JSON!"); + throw new IllegalStateException("Timestamp missing timezone in JSON!"); } - MTLog.w(LOG_TAG, "Timestamp missing timezone in JSON (using device TZ) '%s'!", jTimestamp); - localTimeZoneId = TimeZone.getDefault().getID(); + MTLog.w(LOG_TAG, "Timestamp missing timezone in JSON '%s'!", jTimestamp); } final Timestamp timestamp = new Timestamp(departureInMs, localTimeZoneId); final long originalDepartureDelayMs = jTimestamp.optLong(JSON_ORIGINAL_DEPARTURE_DELAY, 0L); @@ -891,8 +888,10 @@ public JSONObject toJSON() { public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { final JSONObject jTimestamp = new JSONObject(); - jTimestamp.put(JSON_LOCAL_TIME_ZONE_ID, timestamp.localTimeZoneId); jTimestamp.put(JSON_DEPARTURE, timestamp.departureInMs); + if (timestamp.localTimeZoneId != null) { + jTimestamp.put(JSON_LOCAL_TIME_ZONE_ID, timestamp.localTimeZoneId); + } if (timestamp.originalDepartureDelayMs != 0L) { jTimestamp.put(JSON_ORIGINAL_DEPARTURE_DELAY, timestamp.originalDepartureDelayMs); } diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 0061bdda..3e825714 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -1,6 +1,5 @@ package org.mtransit.android.commons.data -import android.annotation.SuppressLint import org.mtransit.android.commons.Constants import org.mtransit.android.commons.floorBy import org.mtransit.android.commons.millisToInstant @@ -270,9 +269,8 @@ fun Schedule.Timestamp.toStringShort() = buildString { if (tripId != null) { append("[tId:").append(tripId).append("]") } - @SuppressLint("DeprecatedCall") - @Suppress("DEPRECATION") // migrating - localTimeZoneId.let { + //noinspection DiscouragedApi + localTimeZoneId?.let { append("[tz:").append(it).append("]") } if (isRealTime) { diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java index 0af44d4f..91fe4fcb 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java @@ -53,6 +53,7 @@ public void setTimestampsAndSort(@NonNull List timestamps) { sortTimestamps(); } + @SuppressWarnings("WeakerAccess") public void sortTimestamps() { CollectionUtils.sort(this.timestamps, Schedule.TIMESTAMPS_COMPARATOR); } @@ -122,7 +123,7 @@ private static ScheduleTimestamps fromExtraJSON(ScheduleTimestamps scheduleTimes final Schedule.Timestamp newTimestamp = Schedule.Timestamp.parseJSON(jTimestamp); if (newTimestamp == null) continue; if (localTimeZoneId == null) { - //noinspection deprecation, DeprecatedCall + //noinspection DiscouragedApi localTimeZoneId = newTimestamp.getLocalTimeZoneId(); } scheduleTimestamps.addTimestampWithoutSort(newTimestamp); From 71572725dab8ea8cfd6881980d9fa99138812f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 10 Aug 2026 09:28:02 -0400 Subject: [PATCH 12/17] wip --- .../commons/provider/GTFSProvider.java | 6 +++++ .../provider/agency/AgencyProvider.java | 25 ++++++++++++++++++- .../agency/AgencyProviderContract.java | 1 + .../provider/bike/BikeStationProvider.java | 6 +++++ .../common/ContentProviderConstants.java | 1 + 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSProvider.java index 3b5a7551..cb06f179 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSProvider.java @@ -561,6 +561,12 @@ public String getAgencyColorString(@NonNull Context context) { return context.getString(R.string.gtfs_rts_color); // do not change to avoid breaking compat w/ old modules } + @Nullable + @Override + public String getAgencyTimeZoneId(@NonNull Context context) { + return context.getString(R.string.gtfs_rts_timezone); + } + /** * Override if multiple {@link GTFSProvider} implementations in same app. */ diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProvider.java b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProvider.java index f150f1e5..aa6cf2a6 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProvider.java @@ -46,6 +46,7 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author uriMatcher.addURI(authority, AgencyProviderContract.DEPLOYED_PATH, ContentProviderConstants.DEPLOYED); uriMatcher.addURI(authority, AgencyProviderContract.LABEL_PATH, ContentProviderConstants.LABEL); uriMatcher.addURI(authority, AgencyProviderContract.COLOR_PATH, ContentProviderConstants.COLOR); + uriMatcher.addURI(authority, AgencyProviderContract.TIMEZONE_ID_PATH, ContentProviderConstants.TIMEZONE_ID); uriMatcher.addURI(authority, AgencyProviderContract.SHORT_NAME_PATH, ContentProviderConstants.SHORT_NAME); uriMatcher.addURI(authority, AgencyProviderContract.SETUP_REQUIRED_PATH, ContentProviderConstants.SETUP_REQUIRED); uriMatcher.addURI(authority, AgencyProviderContract.AREA_PATH, ContentProviderConstants.AREA); @@ -70,6 +71,8 @@ public Cursor queryMT(@NonNull Uri uri, @Nullable String[] projection, @Nullable return getLabel(); case ContentProviderConstants.COLOR: return getColor(); + case ContentProviderConstants.TIMEZONE_ID: + return getTimeZoneId(); case ContentProviderConstants.SHORT_NAME: return getShortName(); case ContentProviderConstants.DEPLOYED: @@ -127,6 +130,7 @@ public String getSortOrder(@NonNull Uri uri) { case ContentProviderConstants.DEPLOYED: case ContentProviderConstants.LABEL: case ContentProviderConstants.COLOR: + case ContentProviderConstants.TIMEZONE_ID: case ContentProviderConstants.SHORT_NAME: case ContentProviderConstants.VERSION: case ContentProviderConstants.SETUP_REQUIRED: @@ -150,6 +154,7 @@ public String getTypeMT(@NonNull Uri uri) { case ContentProviderConstants.DEPLOYED: case ContentProviderConstants.LABEL: case ContentProviderConstants.COLOR: + case ContentProviderConstants.TIMEZONE_ID: case ContentProviderConstants.SHORT_NAME: case ContentProviderConstants.VERSION: case ContentProviderConstants.SETUP_REQUIRED: @@ -172,6 +177,7 @@ private Cursor getAll() { VERSION_PATH, LABEL_PATH, COLOR_PATH, + TIMEZONE_ID_PATH, SHORT_NAME_PATH, DEPLOYED_PATH, SETUP_REQUIRED_PATH, @@ -186,6 +192,7 @@ private Cursor getAll() { getAgencyVersion(), getAgencyLabel(), getAgencyColor(), + getAgencyTimeZoneId(), getAgencyShortName(), isAgencyDeployedInt(), isAgencySetupRequiredInt(), @@ -226,11 +233,12 @@ private String getAgencyLabel() { @NonNull public Cursor getColor() { - MatrixCursor matrixCursor = new MatrixCursor(new String[]{COLOR_PATH}); + final MatrixCursor matrixCursor = new MatrixCursor(new String[]{COLOR_PATH}); matrixCursor.addRow(new Object[]{getAgencyColor()}); return matrixCursor; } + @Nullable private String getAgencyColor() { return getAgencyColorString(requireContextCompat()); } @@ -238,6 +246,21 @@ private String getAgencyColor() { @Nullable public abstract String getAgencyColorString(@NonNull Context context); + @NonNull + public Cursor getTimeZoneId() { + final MatrixCursor matrixCursor = new MatrixCursor(new String[]{TIMEZONE_ID_PATH}); + matrixCursor.addRow(new Object[]{getAgencyTimeZoneId()}); + return matrixCursor; + } + + @Nullable + private String getAgencyTimeZoneId() { + return getAgencyTimeZoneId(requireContextCompat()); + } + + @Nullable + public abstract String getAgencyTimeZoneId(@NonNull Context context); + private Cursor getShortName() { MatrixCursor matrixCursor = new MatrixCursor(new String[]{SHORT_NAME_PATH}); matrixCursor.addRow(new Object[]{getAgencyShortName()}); diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProviderContract.java b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProviderContract.java index f01ecf17..49d271f0 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProviderContract.java +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyProviderContract.java @@ -10,6 +10,7 @@ public interface AgencyProviderContract extends ProviderContract { String VERSION_PATH = "version"; String LABEL_PATH = "label"; String COLOR_PATH = "color"; + String TIMEZONE_ID_PATH = "tz"; String SHORT_NAME_PATH = "shortName"; String DEPLOYED_PATH = "deployed"; String SETUP_REQUIRED_PATH = "setupRequired"; diff --git a/src/main/java/org/mtransit/android/commons/provider/bike/BikeStationProvider.java b/src/main/java/org/mtransit/android/commons/provider/bike/BikeStationProvider.java index 5b2ce979..c0e9ddb1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/bike/BikeStationProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/bike/BikeStationProvider.java @@ -469,6 +469,12 @@ public String getAgencyColorString(@NonNull Context context) { return context.getString(R.string.bike_station_color); } + @Nullable + @Override + public String getAgencyTimeZoneId(@NonNull Context context) { + return context.getString(R.string.bike_station_timezone); + } + /** * Override if multiple {@link BikeStationProvider} implementations in same app. */ diff --git a/src/main/java/org/mtransit/android/commons/provider/common/ContentProviderConstants.java b/src/main/java/org/mtransit/android/commons/provider/common/ContentProviderConstants.java index cd7a5b84..d276a693 100644 --- a/src/main/java/org/mtransit/android/commons/provider/common/ContentProviderConstants.java +++ b/src/main/java/org/mtransit/android/commons/provider/common/ContentProviderConstants.java @@ -18,6 +18,7 @@ public final class ContentProviderConstants { public static final int DEPLOYED = 102; public static final int LABEL = 103; public static final int COLOR = 114; + public static final int TIMEZONE_ID = 119; public static final int SHORT_NAME = 109; public static final int SETUP_REQUIRED = 104; // From dfaeda8504ae6e9d90ee2555526414d9097c48af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 10:49:28 -0400 Subject: [PATCH 13/17] latest --- .../commons/data/RouteDirectionStop.java | 5 +- .../mtransit/android/commons/data/Stop.java | 55 ++++++++----------- .../provider/GTFSProviderContract.java | 3 + .../provider/GTFSRealTimeProvider.java | 5 +- .../commons/provider/StmInfoApiProvider.java | 1 + .../info/stm/StmInfoServiceUpdateProvider.kt | 4 +- .../provider/gtfs/GTFSPOIProvider.java | 1 + .../provider/gtfs/GTFSProviderDbHelper.java | 1 + .../provider/gtfs/GTFSRDSProvider.java | 3 + .../status/GTFSRealTimeTripUpdatesProvider.kt | 5 +- .../data/RouteDirectionStopTestFixtures.kt | 30 +++++++--- .../GTFSRealTimeTripUpdatesProviderTests.kt | 13 ++--- 12 files changed, 68 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java b/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java index f3c4d7e9..e368e38d 100644 --- a/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java +++ b/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java @@ -226,7 +226,7 @@ public String toStringShort() { @Override public JSONObject toJSON() { try { - JSONObject json = new JSONObject(); + final JSONObject json = new JSONObject(); json.put(JSON_ROUTE, Route.toJSON(getRoute())); json.put(JSON_DIRECTION, Direction.toJSON(getDirection())); json.put(JSON_STOP, Stop.toJSON(getStop())); @@ -332,7 +332,8 @@ public static RouteDirectionStop fromCursorStatic(@NonNull Cursor c, @NonNull St CursorExtKt.getDouble(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LAT), CursorExtKt.getDouble(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG), CursorExtKt.optIntNN(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE, Accessibility.DEFAULT), - CursorExtKt.optInt(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH) + CursorExtKt.optInt(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH), + CursorExtKt.optString(c, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID, null) ), CursorExtKt.getBoolean(c, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP), CursorExtKt.optBoolean(c, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_ALWAYS_LAST_TRIP_STOP) diff --git a/src/main/java/org/mtransit/android/commons/data/Stop.java b/src/main/java/org/mtransit/android/commons/data/Stop.java index b2236bdc..278d8246 100644 --- a/src/main/java/org/mtransit/android/commons/data/Stop.java +++ b/src/main/java/org/mtransit/android/commons/data/Stop.java @@ -36,34 +36,18 @@ public class Stop { @Nullable private final Integer originalIdHash; - - @Deprecated - public Stop(int id, - @NonNull String code, - @NonNull String name, - double lat, - double lng) { - this(id, code, name, lat, lng, Accessibility.DEFAULT, GTFSCommons.DEFAULT_ID_HASH); - } - - @Deprecated - public Stop(int id, - @NonNull String code, - @NonNull String name, - double lat, - double lng, - int accessible - ) { - this(id, code, name, lat, lng, accessible, GTFSCommons.DEFAULT_ID_HASH); - } - - public Stop(int id, - @NonNull String code, - @NonNull String name, - double lat, - double lng, - int accessible, - @Nullable Integer originalIdHash + @Nullable + private final String timeZoneId; + + public Stop( + int id, + @NonNull String code, + @NonNull String name, + double lat, + double lng, + int accessible, + @Nullable Integer originalIdHash, + @Nullable String timeZoneId ) { this.id = id; this.code = code; @@ -72,6 +56,7 @@ public Stop(int id, this.lng = lng; this.accessible = accessible; this.originalIdHash = originalIdHash; + this.timeZoneId = timeZoneId; } public Stop(@NonNull Stop stop) { @@ -82,7 +67,8 @@ public Stop(@NonNull Stop stop) { stop.lat, stop.lng, stop.accessible, - stop.originalIdHash + stop.originalIdHash, + stop.timeZoneId ); } @@ -95,7 +81,8 @@ public static Stop fromCursor(@NonNull Cursor c) { CursorExtKt.getDouble(c, GTFSProviderContract.StopColumns.T_STOP_K_LAT), CursorExtKt.getDouble(c, GTFSProviderContract.StopColumns.T_STOP_K_LNG), CursorExtKt.optIntNN(c, GTFSProviderContract.StopColumns.T_STOP_K_ACCESSIBLE, Accessibility.DEFAULT), - CursorExtKt.optInt(c, GTFSProviderContract.StopColumns.T_STOP_K_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH) + CursorExtKt.optInt(c, GTFSProviderContract.StopColumns.T_STOP_K_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH), + CursorExtKt.optString(c, GTFSProviderContract.StopColumns.T_STOP_K_TIMEZONE_ID, null) ); } @@ -110,6 +97,7 @@ public String toString() { ", lng=" + lng + ", a11y=" + accessible + ", odIDHash=" + originalIdHash + + ", tz='" + timeZoneId + '\'' + '}'; } @@ -120,6 +108,7 @@ public String toString() { private static final String JSON_LNG = "lng"; private static final String JSON_ACCESSIBLE = "a11y"; private static final String JSON_ORIGINAL_ID_HASH = "o_id_hash"; + private static final String JSON_TIMEZONE_ID = "tz"; @Nullable public static JSONObject toJSON(@NonNull Stop stop) { @@ -133,6 +122,9 @@ public static JSONObject toJSON(@NonNull Stop stop) { ; jStop.put(JSON_ACCESSIBLE, stop.getAccessible()); jStop.put(JSON_ORIGINAL_ID_HASH, stop.getOriginalIdHash()); + if (stop.timeZoneId != null) { + jStop.put(JSON_TIMEZONE_ID, stop.timeZoneId); + } return jStop; } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while converting to JSON (%s)!", stop); @@ -150,7 +142,8 @@ public static Stop fromJSON(@NonNull JSONObject jStop) throws JSONException { jStop.getDouble(JSON_LAT), jStop.getDouble(JSON_LNG), JSONUtils.optInt(jStop, JSON_ACCESSIBLE, Accessibility.DEFAULT), - JSONUtils.optInt(jStop, JSON_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH) + JSONUtils.optInt(jStop, JSON_ORIGINAL_ID_HASH, GTFSCommons.DEFAULT_ID_HASH), + JSONUtils.optString(jStop, JSON_TIMEZONE_ID) ); } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while parsing JSON '%s'!", jStop); diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java index a81a7219..1d227372 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java @@ -133,6 +133,7 @@ class RouteDirectionStopColumns { public static final String T_STOP_K_LNG = T_STOP + "_" + "lng"; public static final String T_STOP_K_ACCESSIBLE = T_STOP + "_" + "a11y"; public static final String T_STOP_K_ORIGINAL_ID_HASH = T_STOP + "_" + "o_id_hash"; + public static final String T_STOP_K_TIMEZONE_ID = T_STOP + "_" + "tz"; private static final String T_DIRECTION_STOPS = "trip_stops"; // do not change to avoid breaking compat w/ old modules public static final String T_DIRECTION_STOPS_K_STOP_SEQUENCE = T_DIRECTION_STOPS + "_" + "stop_sequence"; public static final String T_DIRECTION_STOPS_K_NO_PICKUP = T_DIRECTION_STOPS + "_" + "decent_only"; @@ -147,6 +148,7 @@ class StopColumns { public static final String T_STOP_K_LNG = "lng"; public static final String T_STOP_K_ACCESSIBLE = "a11y"; public static final String T_STOP_K_ORIGINAL_ID_HASH = "o_id_hash"; + public static final String T_STOP_K_TIMEZONE_ID = "tz"; } class DirectionColumns { @@ -170,6 +172,7 @@ class DirectionStopColumns { public static final String T_STOP_K_LNG = T_STOP + "_" + "lng"; public static final String T_STOP_K_ACCESSIBLE = T_STOP + "_" + "a11y"; public static final String T_STOP_K_ORIGINAL_ID_HASH = T_STOP + "_" + "o_id_hash"; + public static final String T_STOP_K_TIMEZONE_ID = T_STOP + "_" + "tz"; private static final String T_DIRECTION_STOPS = "trip_stops"; // do not change to avoid breaking compat w/ old modules public static final String T_DIRECTION_STOPS_K_STOP_SEQUENCE = T_DIRECTION_STOPS + "_" + "stop_sequence"; public static final String T_DIRECTION_STOPS_K_NO_PICKUP = T_DIRECTION_STOPS + "_" + "decent_only"; diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index f994c903..bcf6d079 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -829,9 +829,10 @@ private synchronized void updateAgencyServiceUpdateDataIfRequiredSync(@NonNull C deleteAllRequired = true; // too old to display } final long minUpdateMs = Math.min(getServiceUpdateMaxValidityInMs(), getServiceUpdateValidityInMs(inFocus)); - if (deleteAllRequired || lastUpdateInMs + minUpdateMs < nowInMs) { - updateAllAgencyServiceUpdateDataFromWWW(context, deleteAllRequired); // try to update + if (!deleteAllRequired && nowInMs <= lastUpdateInMs + minUpdateMs) { + return; } + updateAllAgencyServiceUpdateDataFromWWW(context, deleteAllRequired); // try to update } private void updateAllAgencyServiceUpdateDataFromWWW(@NonNull Context context, boolean deleteAllRequired) { diff --git a/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java b/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java index 5980f66a..25e1d1c1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java @@ -948,6 +948,7 @@ private void loadRealTimeServiceUpdateFromWWW(@NonNull Context context, RouteDir } } + @Discouraged(message = "Using GTFS-RT Trip Updates instead") private synchronized void deleteOldAndCacheNewServiceUpdates(ServiceUpdates serviceUpdates) { // SYNC because may have multiple concurrent same route call if (serviceUpdates != null) { for (ServiceUpdate serviceUpdate : serviceUpdates) { diff --git a/src/main/java/org/mtransit/android/commons/provider/ca/info/stm/StmInfoServiceUpdateProvider.kt b/src/main/java/org/mtransit/android/commons/provider/ca/info/stm/StmInfoServiceUpdateProvider.kt index 32b42321..917eb866 100644 --- a/src/main/java/org/mtransit/android/commons/provider/ca/info/stm/StmInfoServiceUpdateProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/ca/info/stm/StmInfoServiceUpdateProvider.kt @@ -97,9 +97,7 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable { deleteAllRequired = true // too old to display } val minUpdate = serviceUpdateMaxValidity.coerceAtMost(getServiceUpdateValidity(inFocus)) - if (!deleteAllRequired && lastUpdate + minUpdate >= now) { - return - } + if (!deleteAllRequired && lastUpdate + minUpdate >= now) return updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java index 650f1b96..0c46e197 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java @@ -210,6 +210,7 @@ private static ArrayMap getNewProjectionMap(String authority, in sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP); diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDbHelper.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDbHelper.java index d3bd6562..4224bba8 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDbHelper.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDbHelper.java @@ -78,6 +78,7 @@ public String getLogTag() { static final String T_STOP_K_LNG = GTFSCommons.T_STOP_K_LNG; static final String T_STOP_K_ACCESSIBLE = GTFSCommons.T_STOP_K_ACCESSIBLE; static final String T_STOP_K_ORIGINAL_ID_HASH = GTFSCommons.T_STOP_K_ORIGINAL_ID_HASH; + static final String T_STOP_K_TIMEZONE_ID = GTFSCommons.T_STOP_K_TIMEZONE_ID; private static final int[] T_STOP_STRINGS_COLUMN_IDX = GTFSCommons.T_STOP_STRINGS_COLUMN_IDX; private static final String T_STOP_SQL_CREATE = GTFSCommons.getT_STOP_SQL_CREATE(); private static final String T_STOP_SQL_INSERT = GTFSCommons.getT_STOP_SQL_INSERT(); diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java index 36c5487e..eb0a1840 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java @@ -90,6 +90,7 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.StopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.StopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.StopColumns.T_STOP_K_ORIGINAL_ID_HASH); + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.StopColumns.T_STOP_K_TIMEZONE_ID); STOP_PROJECTION_MAP = sb.build(); } @@ -104,6 +105,7 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP); @@ -154,6 +156,7 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.DirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.DirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.DirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.DirectionStopColumns.T_STOP_K_TIMEZONE_ID); // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.DirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.DirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP); diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index 9f206362..8ab7787a 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -428,9 +428,8 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { deleteAllRequired = true // too old to display } val minUpdateMs = min(statusMaxValidityInMs, getStatusValidityInMs(inFocus)) - if (deleteAllRequired || lastUpdateInMs + minUpdateMs < nowInMs) { - updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update - } + if (!deleteAllRequired && nowInMs <= lastUpdateInMs + minUpdateMs) return + updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update } private fun GTFSRealTimeProvider.updateAllAgencyDataFromWWW(context: Context, deleteAllRequired: Boolean) { diff --git a/src/test/java/org/mtransit/android/commons/data/RouteDirectionStopTestFixtures.kt b/src/test/java/org/mtransit/android/commons/data/RouteDirectionStopTestFixtures.kt index 11fe73de..98ceda4b 100644 --- a/src/test/java/org/mtransit/android/commons/data/RouteDirectionStopTestFixtures.kt +++ b/src/test/java/org/mtransit/android/commons/data/RouteDirectionStopTestFixtures.kt @@ -10,7 +10,8 @@ fun makeRDS( originalDirectionId: Int? = 1, directionId: Long = originalDirectionId?.let { routeId * 100L + it } ?: (routeId * 100L + 9L), stopId: Int = 1, - stopOriginalIdHash: Int? = stopId.toString().hashCode() // stopId, // "$stopId".hashCode() + stopOriginalIdHash: Int? = stopId.toString().hashCode(), // stopId, // "$stopId".hashCode() + stopTimeZoneId: String? = "UTC", ) = RouteDirectionStop( 1, Route( @@ -29,19 +30,30 @@ fun makeRDS( "Head-Sign $originalDirectionId", routeId, ), - Stop( - stopId, - "#$stopId", - "Stop #$stopId", - 1.0, - 2.0, - Accessibility.DEFAULT, - stopOriginalIdHash, + makeStop( + stopId = stopId, + stopOriginalIdHash = stopOriginalIdHash, + stopTimeZoneId = stopTimeZoneId, ), false, false, ) +fun makeStop( + stopId: Int = 1, + stopOriginalIdHash: Int? = stopId.toString().hashCode(), // stopId, // "$stopId".hashCode() + stopTimeZoneId: String? = "UTC", +) = Stop( + stopId, + "#$stopId", + "Stop #$stopId", + 1.0, + 2.0, + Accessibility.DEFAULT, + stopOriginalIdHash, + stopTimeZoneId, +) + fun Route.getGTFSRTTargetUUID(): String = GTFSRealTimeProvider.getAgencyRouteTagTargetUUID( authority, diff --git a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt index 8dc52a81..53fd197a 100644 --- a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt +++ b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt @@ -14,6 +14,7 @@ import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.departureArrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.data.makeSchedule +import org.mtransit.android.commons.data.makeStop import org.mtransit.android.commons.data.toScheduleTimestamp import org.mtransit.android.commons.provider.gtfs.GTFSStatusProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.delayDuration @@ -1290,14 +1291,10 @@ class GTFSRealTimeTripUpdatesProviderTests { "headsign", 1 ), - Stop( - stopId, - "#$stopId", - "Stop #$stopId", - 1.0, - 2.0, - Accessibility.DEFAULT, - stopId, // "$stopId".hashCode() + makeStop( + stopId = stopId, + stopOriginalIdHash = stopId, // "$stopId".hashCode() + stopTimeZoneId = LOCAL_TZ_ID ), false, false, From 67e82a06fb55a26deec7b9f0f2fcdf111385539c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 11:13:04 -0400 Subject: [PATCH 14/17] wip --- src/main/java/org/mtransit/android/commons/data/Stop.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/mtransit/android/commons/data/Stop.java b/src/main/java/org/mtransit/android/commons/data/Stop.java index 278d8246..51ec6063 100644 --- a/src/main/java/org/mtransit/android/commons/data/Stop.java +++ b/src/main/java/org/mtransit/android/commons/data/Stop.java @@ -205,6 +205,11 @@ public boolean isSameOriginalId(@Nullable String cleanedOriginalIdHash) { } + @Nullable + public String getTimeZoneId() { + return this.timeZoneId; + } + @Override public boolean equals(Object o) { if (o == null) return false; From 414fb16b3b604cf2caa6069b73806f2592f96643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 11:40:37 -0400 Subject: [PATCH 15/17] wip --- .../mtransit/android/commons/provider/GTFSProviderContract.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java index 1d227372..2e7cbbe2 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java @@ -53,6 +53,7 @@ static String[] makePROJECTION_ROUTE_DIRECTION_STOP() { projection.add(RouteDirectionStopColumns.T_STOP_K_LNG); projection.add(RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); projection.add(RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); + projection.add(RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); return projection.toArray(new String[0]); } From 5353a316cf98752e75c5f58909b5e31e51e05eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 14:04:11 -0400 Subject: [PATCH 16/17] wip --- .../java/org/mtransit/android/commons/data/Stop.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/Stop.java b/src/main/java/org/mtransit/android/commons/data/Stop.java index 51ec6063..49f91c50 100644 --- a/src/main/java/org/mtransit/android/commons/data/Stop.java +++ b/src/main/java/org/mtransit/android/commons/data/Stop.java @@ -3,6 +3,7 @@ import static org.mtransit.android.commons.StringUtils.EMPTY; import android.database.Cursor; +import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -206,7 +207,8 @@ public boolean isSameOriginalId(@Nullable String cleanedOriginalIdHash) { } @Nullable - public String getTimeZoneId() { + public String getTimeZoneIdOrNull() { + if (TextUtils.isEmpty(this.timeZoneId)) return null; return this.timeZoneId; } @@ -221,7 +223,9 @@ public boolean equals(Object o) { && Double.compare(lat, stop.lat) == 0 && Double.compare(lng, stop.lng) == 0 && accessible == stop.accessible - && Objects.equals(originalIdHash, stop.originalIdHash); + && Objects.equals(originalIdHash, stop.originalIdHash) + && Objects.equals(timeZoneId, stop.timeZoneId) + ; } @Override @@ -233,7 +237,8 @@ public int hashCode() { lat, lng, accessible, - originalIdHash + originalIdHash, + timeZoneId ); } } From 83c3683dc285d7aed9eda5f05d12a74f6f5c1af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 14:23:49 -0400 Subject: [PATCH 17/17] wip --- .../android/commons/data/RouteDirectionStop.java | 3 +++ .../java/org/mtransit/android/commons/data/Stop.java | 7 +++++-- .../commons/provider/GTFSProviderContract.java | 4 +++- .../commons/provider/gtfs/GTFSPOIProvider.java | 4 +++- .../commons/provider/gtfs/GTFSRDSProvider.java | 12 +++++++++--- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java b/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java index e368e38d..4d5813ea 100644 --- a/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java +++ b/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java @@ -291,6 +291,9 @@ public ContentValues toContentValues() { values.put(GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG, getStop().getLng()); values.put(GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE, getStop().getAccessible()); values.put(GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH, getStop().getOriginalIdHash()); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + values.put(GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID, getStop().getTimeZoneIdOrNull()); + } // T_DIRECTION_STOPS_K_STOP_SEQUENCE not used in RouteDirectionStop class values.put(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP, SqlUtils.toSQLBoolean(isNoPickup())); if (FeatureFlags.F_EXPORT_DIRECTION_STOP_LAST) { diff --git a/src/main/java/org/mtransit/android/commons/data/Stop.java b/src/main/java/org/mtransit/android/commons/data/Stop.java index 49f91c50..35944e4d 100644 --- a/src/main/java/org/mtransit/android/commons/data/Stop.java +++ b/src/main/java/org/mtransit/android/commons/data/Stop.java @@ -14,6 +14,7 @@ import org.mtransit.android.commons.JSONUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.provider.GTFSProviderContract; +import org.mtransit.commons.FeatureFlags; import org.mtransit.commons.GTFSCommons; import java.util.Objects; @@ -123,8 +124,10 @@ public static JSONObject toJSON(@NonNull Stop stop) { ; jStop.put(JSON_ACCESSIBLE, stop.getAccessible()); jStop.put(JSON_ORIGINAL_ID_HASH, stop.getOriginalIdHash()); - if (stop.timeZoneId != null) { - jStop.put(JSON_TIMEZONE_ID, stop.timeZoneId); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + if (stop.timeZoneId != null) { + jStop.put(JSON_TIMEZONE_ID, stop.timeZoneId); + } } return jStop; } catch (JSONException jsone) { diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java index 2e7cbbe2..72e302dd 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderContract.java @@ -53,7 +53,9 @@ static String[] makePROJECTION_ROUTE_DIRECTION_STOP() { projection.add(RouteDirectionStopColumns.T_STOP_K_LNG); projection.add(RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); projection.add(RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); - projection.add(RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + projection.add(RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + } return projection.toArray(new String[0]); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java index 0c46e197..c2d46aa1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSPOIProvider.java @@ -210,7 +210,9 @@ private static ArrayMap getNewProjectionMap(String authority, in sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); - sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + } // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP); diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java index eb0a1840..719ccbab 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProvider.java @@ -90,7 +90,9 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.StopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.StopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.StopColumns.T_STOP_K_ORIGINAL_ID_HASH); - sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.StopColumns.T_STOP_K_TIMEZONE_ID); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.StopColumns.T_STOP_K_TIMEZONE_ID); + } STOP_PROJECTION_MAP = sb.build(); } @@ -105,7 +107,9 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); - sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.RouteDirectionStopColumns.T_STOP_K_TIMEZONE_ID); + } // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP); @@ -156,7 +160,9 @@ public static void append(@NonNull UriMatcher uriMatcher, @NonNull String author sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_LNG, GTFSProviderContract.DirectionStopColumns.T_STOP_K_LNG); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ACCESSIBLE, GTFSProviderContract.DirectionStopColumns.T_STOP_K_ACCESSIBLE); sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_ORIGINAL_ID_HASH, GTFSProviderContract.DirectionStopColumns.T_STOP_K_ORIGINAL_ID_HASH); - sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.DirectionStopColumns.T_STOP_K_TIMEZONE_ID); + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + sb.appendTableColumn(GTFSProviderDbHelper.T_STOP, GTFSProviderDbHelper.T_STOP_K_TIMEZONE_ID, GTFSProviderContract.DirectionStopColumns.T_STOP_K_TIMEZONE_ID); + } // sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_STOP_SEQUENCE, GTFSProviderContract.DirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE); sb.appendTableColumn(GTFSProviderDbHelper.T_DIRECTION_STOPS, GTFSProviderDbHelper.T_DIRECTION_STOPS_K_NO_PICKUP, GTFSProviderContract.DirectionStopColumns.T_DIRECTION_STOPS_K_NO_PICKUP);