diff --git a/build.gradle b/build.gradle index 9f9d09e2..4b9c2c9e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ android { namespace = "org.mtransit.android.commons" defaultConfig { - consumerProguardFiles("proguard-rules.pro") + consumerProguardFiles("consumer-proguard-rules.pro") minSdk = libs.versions.sdk.min.get().toInteger() targetSdk = libs.versions.sdk.target.get().toInteger() diff --git a/proguard-rules.pro b/consumer-proguard-rules.pro similarity index 100% rename from proguard-rules.pro rename to consumer-proguard-rules.pro diff --git a/src/main/java/org/mtransit/android/commons/SqlUtils.java b/src/main/java/org/mtransit/android/commons/SqlUtils.java index 6be41108..1ca6f129 100644 --- a/src/main/java/org/mtransit/android/commons/SqlUtils.java +++ b/src/main/java/org/mtransit/android/commons/SqlUtils.java @@ -173,8 +173,8 @@ public static String unescapeString(@NonNull String string) { } @Nullable - public static String unescapeStringOrNull(@NonNull String string) { - return SQLUtils.unescapeStringOrNull(string); + public static String unquoteUnescapeStringOrNull(@NonNull String string) { + return SQLUtils.unquoteUnescapeStringOrNull(string); } private SqlUtils() { diff --git a/src/main/java/org/mtransit/android/commons/TimeUtils.java b/src/main/java/org/mtransit/android/commons/TimeUtils.java index 3f5ed20f..781ad03f 100644 --- a/src/main/java/org/mtransit/android/commons/TimeUtils.java +++ b/src/main/java/org/mtransit/android/commons/TimeUtils.java @@ -208,34 +208,6 @@ private static DateFormat getShortDateTimeFormatter() { @NonNull public static String formatSimpleDuration(long durationInMs) { - StringBuilder sb = new StringBuilder(); - if (durationInMs < 0) { - durationInMs = abs(durationInMs); - } - sb.append("-"); - final long days = durationInMs / TimeUnit.DAYS.toMillis(1L); - if (days > 0) { - sb.append(" ").append(days).append(" days"); - durationInMs = durationInMs % days; - } - final long hours = durationInMs / TimeUnit.HOURS.toMillis(1L); - if (hours > 0) { - sb.append(" ").append(hours).append(" h"); - durationInMs = durationInMs % hours; - } - final long minutes = durationInMs / TimeUnit.MINUTES.toMillis(1L); - if (minutes > 0) { - sb.append(" ").append(minutes).append(" min"); - durationInMs = durationInMs % minutes; - } - final long seconds = durationInMs / TimeUnit.MINUTES.toMillis(1L); - if (seconds > 0) { - sb.append(" ").append(seconds).append(" sec"); - durationInMs = durationInMs % seconds; - } - if (durationInMs > 0) { - sb.append(" ").append(durationInMs).append(" ms"); - } - return sb.toString(); + return TimeUtilsKt.formatSimpleDuration(durationInMs); } } diff --git a/src/main/java/org/mtransit/android/commons/TimeUtils.kt b/src/main/java/org/mtransit/android/commons/TimeUtils.kt new file mode 100644 index 00000000..d0e638e9 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/TimeUtils.kt @@ -0,0 +1,17 @@ +package org.mtransit.android.commons + +import kotlin.math.abs +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds + +fun formatSimpleDuration(durationInMs: Long) = buildString { + val negative = durationInMs < 0 + abs(durationInMs).milliseconds.toComponents { days, hours, minutes, seconds, nanoseconds -> + days.takeIf { it > 0 }?.let { append(it).append(" days ") } + hours.takeIf { it > 0 }?.let { append(it).append(" h ") } + minutes.takeIf { it > 0 }?.let { append(it).append(" min ") } + seconds.takeIf { it > 0 }?.let { append(it).append(" sec ") } + nanoseconds.takeIf { it > 0 }?.nanoseconds?.inWholeMilliseconds?.let { append(it).append(" ms ") } + } + if (negative) insert(0, "-") +}.trim() 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 6f1f9b18..9f2cbbff 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -434,6 +434,10 @@ public String getLogTag() { private Boolean oldSchedule = null; @Nullable private Integer accessible = null; + @Nullable + private String tripId = null; // will store trip ID int initially but replaced with real trip ID soon after + @Nullable + private Long arrivalDiffMs = null; @VisibleForTesting public Timestamp(long t) { @@ -453,6 +457,28 @@ public long getT() { return t; } + public long getArrivalT() { + return t + (arrivalDiffMs == null ? 0L : arrivalDiffMs); + } + + @Nullable + public Long getArrivalTIfDifferent() { + return arrivalDiffMs == null ? null : t + arrivalDiffMs; + } + + public void setArrivalTimestamp(long arrivalTimestamp) { + setArrivalDiffMs(arrivalTimestamp - this.t); + } + + public void setArrivalDiffMs(@Nullable Long arrivalDiffMs) { + this.arrivalDiffMs = arrivalDiffMs; + } + + @Nullable + public Long getArrivalDiffMs() { + return arrivalDiffMs; + } + @NonNull public Timestamp setHeadsign(@Direction.HeadSignType int headsignType, @Nullable String headsignValue) { this.headsignType = headsignType; @@ -600,6 +626,15 @@ public int getAccessibleOrDefault() { return this.accessible == null ? Accessibility.DEFAULT : this.accessible; } + public void setTripId(@Nullable String tripId) { + this.tripId = tripId; + } + + @Nullable + public String getTripId() { + return tripId; + } + @SuppressWarnings("RedundantIfStatement") @Override public boolean equals(Object o) { @@ -615,6 +650,8 @@ public boolean equals(Object o) { if (!Objects.equals(realTime, timestamp.realTime)) return false; if (!Objects.equals(oldSchedule, timestamp.oldSchedule)) return false; if (!Objects.equals(accessible, timestamp.accessible)) return false; + if (!Objects.equals(tripId, timestamp.tripId)) return false; + if (!Objects.equals(arrivalDiffMs, timestamp.arrivalDiffMs)) return false; // if (!Objects.equals(heading, timestamp.heading)) return false; // LAZY return true; } @@ -628,6 +665,8 @@ public int hashCode() { result = 31 * result + (realTime != null ? realTime.hashCode() : 0); result = 31 * result + (oldSchedule != null ? oldSchedule.hashCode() : 0); result = 31 * result + (accessible != null ? accessible : 0); + result = 31 * result + (tripId != null ? tripId.hashCode() : 0); + result = 31 * result + (arrivalDiffMs != null ? arrivalDiffMs.hashCode() : 0); // result = 31 * result + (heading != null ? heading.hashCode() : 0); // LAZY return result; } @@ -635,19 +674,40 @@ public int hashCode() { @NonNull @Override public String toString() { - return Timestamp.class.getSimpleName() + "{" + - "t=" + (Constants.DEBUG ? MTLog.formatDateTime(t) : t) + - ", headsignType=" + headsignType + - ", headsignValue='" + headsignValue + '\'' + - ", localTimeZone='" + localTimeZoneId + '\'' + - ", realTime=" + realTime + - ", oldSchedule=" + oldSchedule + - ", accessible=" + accessible + - ", heading='" + heading + '\'' + - '}'; + StringBuilder sb = new StringBuilder(Timestamp.class.getSimpleName()); + sb.append('{'); + sb.append("t=").append(Constants.DEBUG ? MTLog.formatDateTime(t) : t); + if (arrivalDiffMs != null) { + sb.append(", aD:").append(arrivalDiffMs); + } + if (tripId != null) { + sb.append(", tripId:'").append(tripId).append('\''); + } + if (headsignType != Direction.HEADSIGN_TYPE_NONE) { + sb.append(", ht:").append(headsignType); + } + if (headsignValue != null) { + sb.append(", hv:'").append(headsignValue).append('\''); + } + if (localTimeZoneId != null) { + sb.append(", tz:'").append(localTimeZoneId).append('\''); + } + if (realTime != null) { + sb.append(", rt:").append(realTime); + } + if (oldSchedule != null) { + sb.append(", old:").append(oldSchedule); + } + if (accessible != null) { + sb.append(", a11y:").append(accessible); + } + sb.append('}'); + return sb.toString(); } private static final String JSON_TIMESTAMP = "t"; + private static final String JSON_ARRIVAL_DIFF = "tDiffA"; + private static final String JSON_TRIP_ID = "trip_id"; 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"; @@ -658,10 +718,16 @@ public String toString() { @Nullable static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { try { - long t = jTimestamp.getLong(JSON_TIMESTAMP); - Timestamp timestamp = new Timestamp(t); - int headSignType = jTimestamp.optInt(JSON_HEADSIGN_TYPE, -1); - String headSignValue = jTimestamp.optString(JSON_HEADSIGN_VALUE, StringUtils.EMPTY); + final long t = jTimestamp.getLong(JSON_TIMESTAMP); + final Timestamp timestamp = new Timestamp(t); + if (jTimestamp.has(JSON_ARRIVAL_DIFF)) { + timestamp.setArrivalDiffMs(jTimestamp.getLong(JSON_ARRIVAL_DIFF)); + } + if (jTimestamp.has(JSON_TRIP_ID)) { + timestamp.setTripId(jTimestamp.getString(JSON_TRIP_ID)); + } + final int headSignType = jTimestamp.optInt(JSON_HEADSIGN_TYPE, -1); + final String headSignValue = jTimestamp.optString(JSON_HEADSIGN_VALUE, StringUtils.EMPTY); if (headSignType >= 0 && !headSignValue.isEmpty()) { timestamp.setHeadsign(headSignType, headSignValue); } else { @@ -699,6 +765,12 @@ public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { JSONObject jTimestamp = new JSONObject(); jTimestamp.put(JSON_TIMESTAMP, timestamp.t); + if (timestamp.arrivalDiffMs != null) { + jTimestamp.put(JSON_ARRIVAL_DIFF, timestamp.arrivalDiffMs); + } + if (timestamp.tripId != null) { + jTimestamp.put(JSON_TRIP_ID, timestamp.tripId); + } if (timestamp.headsignType != Direction.HEADSIGN_TYPE_NONE && timestamp.headsignValue != null) { jTimestamp.put(JSON_HEADSIGN_TYPE, timestamp.headsignType); jTimestamp.put(JSON_HEADSIGN_VALUE, timestamp.headsignValue); diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderDbHelper.java b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderDbHelper.java index bd25ba6b..09a32994 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSProviderDbHelper.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSProviderDbHelper.java @@ -40,9 +40,9 @@ public String getLogTag() { */ public static final String DB_NAME = "gtfs_rts.db"; // do not change to avoid breaking compat w/ old modules - static final String T_STRINGS = GTFSCommons.T_STRINGS; - static final String T_STRINGS_K_ID = GTFSCommons.T_STRINGS_K_ID; - static final String T_STRINGS_K_STRING = GTFSCommons.T_STRINGS_K_STRING; + public static final String T_STRINGS = GTFSCommons.T_STRINGS; + public static final String T_STRINGS_K_ID = GTFSCommons.T_STRINGS_K_ID; + public static final String T_STRINGS_K_STRING = GTFSCommons.T_STRINGS_K_STRING; private static final String T_STRINGS_SQL_CREATE = GTFSCommons.getT_STRINGS_SQL_CREATE(); private static final String T_STRINGS_SQL_INSERT = GTFSCommons.getT_STRINGS_SQL_INSERT(); private static final String T_STRINGS_SQL_DROP = GTFSCommons.getT_STRINGS_SQL_DROP(); @@ -93,6 +93,13 @@ public String getLogTag() { private static final String T_DIRECTION_STOPS_SQL_INSERT = GTFSCommons.getT_DIRECTION_STOPS_SQL_INSERT(); private static final String T_DIRECTION_STOPS_SQL_DROP = GTFSCommons.getT_DIRECTION_STOPS_SQL_DROP(); + public static final String T_TRIP_IDS = GTFSCommons.T_TRIP_IDS; + public static final String T_TRIP_IDS_K_ID = GTFSCommons.T_TRIP_IDS_K_ID; + public static final String T_TRIP_IDS_K_ID_INT = GTFSCommons.T_TRIP_IDS_K_ID_INT; + private static final String T_TRIP_IDS_SQL_CREATE = GTFSCommons.getT_TRIP_IDS_SQL_CREATE(); + private static final String T_TRIP_IDS_SQL_INSERT = GTFSCommons.getT_TRIP_IDS_SQL_INSERT(); + private static final String T_TRIP_IDS_SQL_DROP = GTFSCommons.getT_TRIP_IDS_SQL_DROP(); + @SuppressWarnings("WeakerAccess") static final String T_SERVICE_IDS = GTFSCommons.T_SERVICE_IDS; @SuppressWarnings("unused") // not used by main app currently @@ -150,6 +157,9 @@ public void onUpgradeMT(@NonNull SQLiteDatabase db, int oldVersion, int newVersi if (FeatureFlags.F_EXPORT_SERVICE_ID_INTS) { db.execSQL(T_SERVICE_IDS_SQL_DROP); } + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { + db.execSQL(T_TRIP_IDS_SQL_DROP); + } db.execSQL(T_SERVICE_DATES_SQL_DROP); db.execSQL(T_ROUTE_DIRECTION_STOP_STATUS_SQL_DROP); initAllDbTables(db, true); @@ -163,8 +173,11 @@ public boolean isDbExist(@NonNull Context context) { private void initAllDbTables(@NonNull SQLiteDatabase db, boolean upgrade) { MTLog.i(this, "Data: deploying DB..."); final int nId = TimeUtils.currentTimeSec(); - final int nbTotalOperations = 8; - int progress = 0; + final long startInMs = TimeUtils.currentTimeMillis(); + int nbTotalOperations = 7; + if (FeatureFlags.F_EXPORT_SERVICE_ID_INTS) nbTotalOperations++; + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) nbTotalOperations++; + int progress = -1; final NotificationManagerCompat nm = NotificationManagerCompat.from(this.context); final boolean notifEnabled = nm.areNotificationsEnabled(); final NotificationCompat.Builder nb; @@ -180,9 +193,9 @@ private void initAllDbTables(@NonNull SQLiteDatabase db, boolean upgrade) { nb = null; } db.execSQL(SQLUtils.PRAGMA_AUTO_VACUUM_NONE); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); final Map allStrings = new HashMap<>(); - if (FeatureFlags.F_EXPORT_STRINGS) { + if (FeatureFlags.F_EXPORT_STRINGS || FeatureFlags.F_EXPORT_SCHEDULE_STRINGS) { initDbTableWithRetry(context, db, T_STRINGS, T_STRINGS_SQL_CREATE, T_STRINGS_SQL_INSERT, T_STRINGS_SQL_DROP, getStringsFiles(), null, null, (id, string) -> { allStrings.put(id, string); @@ -190,28 +203,48 @@ private void initAllDbTables(@NonNull SQLiteDatabase db, boolean upgrade) { } ); // 1st } - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); initDbTableWithRetry(context, db, T_ROUTE, T_ROUTE_SQL_CREATE, T_ROUTE_SQL_INSERT, T_ROUTE_SQL_DROP, getRouteFiles(), allStrings, T_ROUTE_STRINGS_COLUMN_IDX); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); initDbTableWithRetry(context, db, T_DIRECTION, T_DIRECTION_SQL_CREATE, T_DIRECTION_SQL_INSERT, T_DIRECTION_SQL_DROP, getDirectionFiles(), allStrings, T_DIRECTION_STRINGS_COLUMN_IDX); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); initDbTableWithRetry(context, db, T_STOP, T_STOP_SQL_CREATE, T_STOP_SQL_INSERT, T_STOP_SQL_DROP, getStopFiles(), allStrings, T_STOP_STRINGS_COLUMN_IDX); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); initDbTableWithRetry(context, db, T_DIRECTION_STOPS, T_DIRECTION_STOPS_SQL_CREATE, T_DIRECTION_STOPS_SQL_INSERT, T_DIRECTION_STOPS_SQL_DROP, getDirectionStopsFiles()); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); if (FeatureFlags.F_EXPORT_SERVICE_ID_INTS) { initDbTableWithRetry(context, db, T_SERVICE_IDS, T_SERVICE_IDS_SQL_CREATE, T_SERVICE_IDS_SQL_INSERT, T_SERVICE_IDS_SQL_DROP, getServiceIdsFiles()); } - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); initDbTableWithRetry(context, db, T_SERVICE_DATES, T_SERVICE_DATES_SQL_CREATE, T_SERVICE_DATES_SQL_INSERT, T_SERVICE_DATES_SQL_DROP, getServiceDatesFiles()); - if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress++); + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); + initDbTableWithRetry(context, db, T_TRIP_IDS, T_TRIP_IDS_SQL_CREATE, T_TRIP_IDS_SQL_INSERT, T_TRIP_IDS_SQL_DROP, getTripIdsFiles()); + } + if (notifEnabled) NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, ++progress); db.execSQL(T_ROUTE_DIRECTION_STOP_STATUS_SQL_CREATE); if (notifEnabled) { - nb.setSmallIcon(android.R.drawable.stat_notify_sync_noanim); // - NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, progress); + nb.setSmallIcon(android.R.drawable.stat_notify_sync_noanim); + NotificationUtils.setProgressAndNotify(nm, nb, nId, nbTotalOperations, nbTotalOperations); nm.cancel(nId); } - MTLog.i(this, "Data: deploying DB... DONE"); + final long durationInMs = TimeUtils.currentTimeMillis() - startInMs; + MTLog.i(this, "Data: deploying DB... DONE (%s)", MTLog.formatDuration(durationInMs)); + } + + /** + * Override if multiple {@link GTFSProviderDbHelper} implementations in same app. + */ + private int[] getTripIdsFiles() { + if (GTFSCurrentNextProvider.hasCurrentData(context)) { + if (GTFSCurrentNextProvider.isNextData(context)) { + return new int[]{R.raw.next_gtfs_schedule_trip_ids}; + } else { // CURRENT = default + return new int[]{R.raw.current_gtfs_schedule_trip_ids}; + } + } else { + return new int[]{R.raw.gtfs_schedule_trip_ids}; + } } /** diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSScheduleTimestampsProvider.java index a94b2368..0664e8a2 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSScheduleTimestampsProvider.java @@ -15,6 +15,8 @@ import org.mtransit.android.commons.data.Schedule; import org.mtransit.android.commons.data.ScheduleTimestamps; import org.mtransit.android.commons.provider.agency.AgencyUtils; +import org.mtransit.android.commons.provider.gtfs.GTFSStringsUtils; +import org.mtransit.android.commons.provider.gtfs.GTFSTripIdsUtils; import org.mtransit.commons.FeatureFlags; import java.util.ArrayList; @@ -107,9 +109,12 @@ static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider provider, } startsAt.add(Calendar.DATE, +1); // NEXT DAY } - if (FeatureFlags.F_EXPORT_STRINGS) { + if (FeatureFlags.F_EXPORT_STRINGS || FeatureFlags.F_EXPORT_SCHEDULE_STRINGS) { allTimestamps = GTFSStringsUtils.updateStrings(allTimestamps, provider); } + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { + allTimestamps = GTFSTripIdsUtils.updateTripIds(allTimestamps, provider); + } ScheduleTimestamps scheduleTimestamps = new ScheduleTimestamps(rds.getUUID(), startsAtInMs, endsAtInMs); scheduleTimestamps.setSourceLabel(GTFSProvider.getSOURCE_LABEL(provider.requireContextCompat())); scheduleTimestamps.setTimestampsAndSort(allTimestamps); diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSStatusProvider.java index 2913e306..d534666b 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSStatusProvider.java @@ -24,8 +24,12 @@ import org.mtransit.android.commons.data.RouteDirectionStop; import org.mtransit.android.commons.data.Schedule; import org.mtransit.android.commons.provider.agency.AgencyUtils; +import org.mtransit.android.commons.provider.gtfs.GTFSStringsUtils; +import org.mtransit.android.commons.provider.gtfs.GTFSTripIdsUtils; +import org.mtransit.commons.CharUtils; import org.mtransit.commons.FeatureFlags; import org.mtransit.commons.GTFSCommons; +import org.mtransit.commons.sql.SQLUtils; import java.io.BufferedReader; import java.io.InputStream; @@ -219,13 +223,13 @@ private static String getROUTE_FREQUENCY_RAW_FILE_FORMAT(@NonNull Context contex return routeFrequencyRawFileFormat; } - private static final String GTFS_ROUTE_FREQUENCY_FILE_COL_SPLIT_ON = ","; - private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_COUNT = 5; private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_SERVICE_IDX = 0; private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_DIRECTION_IDX = 1; private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_START_TIME_IDX = 2; private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_END_TIME_IDX = 3; private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_HEADWAY_IDX = 4; + // -> + private static final int GTFS_ROUTE_FREQUENCY_FILE_COL_COUNT = 5; @NonNull private static ArrayList findTimestamps(@NonNull GTFSProvider provider, Schedule.ScheduleStatusFilter filter) { @@ -310,9 +314,12 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide } now.add(Calendar.DATE, +1); // NEXT DAY } - if (FeatureFlags.F_EXPORT_STRINGS) { + if (FeatureFlags.F_EXPORT_STRINGS || FeatureFlags.F_EXPORT_SCHEDULE_STRINGS) { allTimestamps = GTFSStringsUtils.updateStrings(allTimestamps, provider); } + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { + allTimestamps = GTFSTripIdsUtils.updateTripIds(allTimestamps, provider); + } return allTimestamps; } @@ -341,15 +348,43 @@ private static String getSTOP_SCHEDULE_RAW_FILE_FORMAT(@NonNull Context context) private static final String STOP_SCHEDULE_RAW_FILE_TYPE = "raw"; - private static final String GTFS_SCHEDULE_STOP_FILE_COL_SPLIT_ON = ","; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_COUNT = 6; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_COUNT_EXTRA = 4; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX = 0; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_DIRECTION_IDX = 1; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_IDX = 2; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_TYPE_IDX = 3; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX = 4; - private static final int GTFS_SCHEDULE_STOP_FILE_COL_ACCESSIBLE_IDX = 5; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_DIRECTION_IDX; + // + private static final int GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_TYPE_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_ACCESSIBLE_IDX; + // -> + private static final int GTFS_SCHEDULE_STOP_FILE_COL_COUNT; + private static final int GTFS_SCHEDULE_STOP_FILE_COL_COUNT_EXTRA; + + static { + int idx = -1; + GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX = ++idx; // 0 + GTFS_SCHEDULE_STOP_FILE_COL_DIRECTION_IDX = ++idx; // 1 + // + GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_IDX = ++idx; // 2 + if (FeatureFlags.F_EXPORT_TRIP_ID) { + if (FeatureFlags.F_EXPORT_ARRIVAL_W_TRIP_ID) { + GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX = ++idx; + } else { + GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX = -1; + } + GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX = ++idx; + } else { + GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX = -1; + GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX = -1; + } + GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_TYPE_IDX = ++idx; + GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX = ++idx; + GTFS_SCHEDULE_STOP_FILE_COL_ACCESSIBLE_IDX = ++idx; + // -> + GTFS_SCHEDULE_STOP_FILE_COL_COUNT = GTFS_SCHEDULE_STOP_FILE_COL_ACCESSIBLE_IDX + 1; + GTFS_SCHEDULE_STOP_FILE_COL_COUNT_EXTRA = GTFS_SCHEDULE_STOP_FILE_COL_COUNT - 2; + } @NonNull static Set findScheduleList( @@ -360,7 +395,7 @@ static Set findScheduleList( String dateS, String timeS, long diffWithRealityInMs ) { - final int timeI = Integer.parseInt(timeS); + final int timeI = FeatureFlags.F_SCHEDULE_IN_MINUTES ? Integer.parseInt(timeS) / 100 : Integer.parseInt(timeS); Set result = new HashSet<>(); final Set> serviceIdOrIntAndExceptionTypes = findServicesAndExceptionTypes(provider, dateS); final Set serviceIdOrInts = filterServiceIdOrInts(serviceIdOrIntAndExceptionTypes, diffWithRealityInMs > 0L); @@ -378,21 +413,23 @@ static Set findScheduleList( InputStream is = context.getResources().openRawResource(fileId); br = new BufferedReader(new InputStreamReader(is, FileUtils.getUTF8()), 8192); String[] lineItems; - String lineServiceIdWithQuotes; String lineServiceIdOrInt; long lineDirectionId; int lineDeparture; int lineDepartureDelta; + String arrivalDiffS; + int arrivalDiff; Long tTimestampInMs; + Long arrivalTimestampMs; Schedule.Timestamp timestamp; + String tripIdOrInt; String headsignTypeS; Integer headsignType; - String headsignValueWithQuotes; String accessibleS; Integer accessible; while ((line = br.readLine()) != null) { try { - lineItems = line.split(GTFS_SCHEDULE_STOP_FILE_COL_SPLIT_ON); + lineItems = line.split(SQLUtils.COLUMN_SEPARATOR); if (lineItems.length < GTFS_SCHEDULE_STOP_FILE_COL_COUNT) { MTLog.w(LOG_TAG, "Cannot parse schedule '%s'!", line); continue; @@ -400,8 +437,7 @@ static Set findScheduleList( if (FeatureFlags.F_EXPORT_SERVICE_ID_INTS) { lineServiceIdOrInt = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX]; } else { - lineServiceIdWithQuotes = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX]; - lineServiceIdOrInt = lineServiceIdWithQuotes.substring(1, lineServiceIdWithQuotes.length() - 1); + lineServiceIdOrInt = SQLUtils.unquotes(lineItems[GTFS_SCHEDULE_STOP_FILE_COL_SERVICE_IDX]); } if (!serviceIdOrInts.contains(lineServiceIdOrInt)) { continue; @@ -410,29 +446,9 @@ static Set findScheduleList( if (directionId != lineDirectionId) { continue; } - lineDeparture = Integer.parseInt(lineItems[GTFS_SCHEDULE_STOP_FILE_COL_DEPARTURE_IDX]); - tTimestampInMs = convertToTimestamp(context, lineDeparture, dateS); - if (lineDeparture > timeI) { - if (tTimestampInMs != null) { - timestamp = new Schedule.Timestamp(tTimestampInMs + diffWithRealityInMs, localTimeZoneId); - headsignTypeS = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_TYPE_IDX]; - headsignType = TextUtils.isEmpty(headsignTypeS) ? null : Integer.valueOf(headsignTypeS); - if (headsignType != null && headsignType >= 0) { - headsignValueWithQuotes = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX]; - timestamp.setHeadsign(headsignType, SqlUtils.unescapeStringOrNull(headsignValueWithQuotes)); - } - timestamp.setOldSchedule(diffWithRealityInMs > 0L); - timestamp.setRealTime(false); // static - accessibleS = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_ACCESSIBLE_IDX]; - accessible = TextUtils.isEmpty(accessibleS) ? null : Integer.valueOf(accessibleS); - if (accessible != null && accessible >= 0) { - timestamp.setAccessible(accessible); - } - result.add(timestamp); - } - } + lineDeparture = 0; // 1st departure contains full time "HHMMSS" final int nbExtra = (lineItems.length - GTFS_SCHEDULE_STOP_FILE_COL_COUNT) / GTFS_SCHEDULE_STOP_FILE_COL_COUNT_EXTRA; - for (int i = 1; i <= nbExtra; i++) { + for (int i = 0; i <= nbExtra; i++) { 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; @@ -440,11 +456,37 @@ static Set findScheduleList( if (lineDeparture > timeI) { if (tTimestampInMs != null) { timestamp = new Schedule.Timestamp(tTimestampInMs + diffWithRealityInMs, localTimeZoneId); + if (FeatureFlags.F_EXPORT_TRIP_ID) { + if (FeatureFlags.F_EXPORT_ARRIVAL_W_TRIP_ID && GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX >= 0) { + arrivalDiffS = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_ARRIVAL_DIFF_IDX + extraIdx]; + if (!TextUtils.isEmpty(arrivalDiffS) && CharUtils.isDigitsOnly(arrivalDiffS)) { + arrivalDiff = Integer.parseInt(arrivalDiffS); + if (arrivalDiff > 0) { + arrivalTimestampMs = convertToTimestamp(context, lineDeparture - arrivalDiff, dateS); + if (arrivalTimestampMs != null) { + timestamp.setArrivalTimestamp(arrivalTimestampMs); + } + } + } + } + if (GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX >= 0) { + if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { + tripIdOrInt = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX + extraIdx]; + } else { + tripIdOrInt = SQLUtils.unquotes(lineItems[GTFS_SCHEDULE_STOP_FILE_COL_TRIP_ID_IDX + extraIdx]); + } + if (!TextUtils.isEmpty(tripIdOrInt)) { + timestamp.setTripId(tripIdOrInt); + } + } + } headsignTypeS = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_TYPE_IDX + extraIdx]; headsignType = TextUtils.isEmpty(headsignTypeS) ? null : Integer.valueOf(headsignTypeS); if (headsignType != null && headsignType >= 0) { - headsignValueWithQuotes = lineItems[GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX + extraIdx]; - timestamp.setHeadsign(headsignType, SqlUtils.unescapeStringOrNull(headsignValueWithQuotes)); + timestamp.setHeadsign( + headsignType, + SqlUtils.unquoteUnescapeStringOrNull(lineItems[GTFS_SCHEDULE_STOP_FILE_COL_HEADSIGN_VALUE_IDX + extraIdx]) + ); } timestamp.setOldSchedule(diffWithRealityInMs > 0L); timestamp.setRealTime(false); // static @@ -595,7 +637,6 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid String fileName = String.format(getROUTE_FREQUENCY_RAW_FILE_FORMAT(context), routeId); InputStream is; String[] lineItems; - String lineServiceIdWithQuotes; String lineServiceIdOrInt; long lineDirectionId; int endTime; @@ -613,7 +654,7 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid br = new BufferedReader(new InputStreamReader(is, FileUtils.getUTF8()), 8192); while ((line = br.readLine()) != null) { try { - lineItems = line.split(GTFS_ROUTE_FREQUENCY_FILE_COL_SPLIT_ON); + lineItems = line.split(SQLUtils.COLUMN_SEPARATOR); if (lineItems.length != GTFS_ROUTE_FREQUENCY_FILE_COL_COUNT) { MTLog.w(LOG_TAG, "Cannot parse frequency '%s'!", line); continue; @@ -621,8 +662,7 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid if (FeatureFlags.F_EXPORT_SERVICE_ID_INTS) { lineServiceIdOrInt = lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_SERVICE_IDX]; } else { - lineServiceIdWithQuotes = lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_SERVICE_IDX]; - lineServiceIdOrInt = lineServiceIdWithQuotes.substring(1, lineServiceIdWithQuotes.length() - 1); + lineServiceIdOrInt = SQLUtils.unquotes(lineItems[GTFS_ROUTE_FREQUENCY_FILE_COL_SERVICE_IDX]); } if (!serviceIdOrInts.contains(lineServiceIdOrInt)) { continue; @@ -664,7 +704,10 @@ private static HashSet findFrequencyList(@NonNull GTFSProvid @Nullable private static Long convertToTimestamp(Context context, int timeInt, String dateS) { try { - Date parsedDate = getToTimestampFormat(context).parseThreadSafe( + if (FeatureFlags.F_SCHEDULE_IN_MINUTES) { + timeInt *= 100; // HHMM -> HHMMSS + } + final Date parsedDate = getToTimestampFormat(context).parseThreadSafe( dateS + String.format(Locale.ENGLISH, TIME_FORMATTER, timeInt) ); return parsedDate == null ? null : parsedDate.getTime(); diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDBHelperUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDBHelperUtils.kt index 691963f3..23cd237e 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDBHelperUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSProviderDBHelperUtils.kt @@ -6,7 +6,6 @@ import org.mtransit.android.commons.FileUtils import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SqlUtils import org.mtransit.android.commons.provider.GTFSProviderDbHelper -import org.mtransit.android.commons.provider.GTFSStringsUtils import org.mtransit.commons.FeatureFlags import org.mtransit.commons.GTFSCommons import java.io.BufferedReader diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSStringsUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStringsUtils.kt similarity index 92% rename from src/main/java/org/mtransit/android/commons/provider/GTFSStringsUtils.kt rename to src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStringsUtils.kt index b3fa7b2a..6fd35a70 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSStringsUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStringsUtils.kt @@ -1,12 +1,16 @@ -package org.mtransit.android.commons.provider +package org.mtransit.android.commons.provider.gtfs import android.database.Cursor import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.provider.GTFSProvider +import org.mtransit.android.commons.provider.GTFSProviderDbHelper +import org.mtransit.commons.FeatureFlags import org.mtransit.commons.GTFSCommons import org.mtransit.commons.sql.SQLUtils import org.mtransit.commons.sql.SQLUtils.quotes import org.mtransit.commons.sql.SQLUtils.unquotes +import kotlin.collections.get object GTFSStringsUtils : MTLog.Loggable { @@ -17,6 +21,7 @@ object GTFSStringsUtils : MTLog.Loggable { @Suppress("DiscouragedApi") @JvmStatic fun > updateStrings(timestamps: T, gtfsProvider: GTFSProvider): T { + if (!FeatureFlags.F_EXPORT_STRINGS && !FeatureFlags.F_EXPORT_SCHEDULE_STRINGS) return timestamps val stringIds = timestamps .mapNotNull { it.headsignValue?.split(GTFSCommons.STRINGS_SEPARATOR) } .flatten() @@ -104,4 +109,4 @@ object GTFSStringsUtils : MTLog.Loggable { }?.joinToString(GTFSCommons.STRINGS_SEPARATOR) ?: stringIds } -} +} \ No newline at end of file diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt new file mode 100644 index 00000000..5d1f2feb --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt @@ -0,0 +1,62 @@ +package org.mtransit.android.commons.provider.gtfs + +import android.database.Cursor +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.provider.GTFSProvider +import org.mtransit.android.commons.provider.GTFSProviderDbHelper +import org.mtransit.commons.FeatureFlags + +object GTFSTripIdsUtils : MTLog.Loggable { + + private val LOG_TAG: String = GTFSTripIdsUtils::class.java.simpleName + + override fun getLogTag() = LOG_TAG + + @Suppress("DiscouragedApi") + @JvmStatic + fun > updateTripIds(timestamps: T, gtfsProvider: GTFSProvider): T { + if (!FeatureFlags.F_EXPORT_TRIP_ID_INTS) return timestamps + val tripIdInts = timestamps + .mapNotNull { it.tripId } + .distinct() + .takeIf { it.isNotEmpty() } + ?: return timestamps + val idIntToIdMap = loadTripIds(gtfsProvider, tripIdInts) + timestamps.forEach { timestamp -> + timestamp.tripId?.let { tripIdInt -> + timestamp.tripId = tripIdInt.toIntOrNull()?.let { idIntToIdMap[it] } ?: tripIdInt + } + } + return timestamps + } + + private fun loadTripIds(gtfsProvider: GTFSProvider, tripIdInts: List): Map { + if (tripIdInts.isEmpty()) return emptyMap() + val placeholders = tripIdInts.joinToString(",") { "?" } + return gtfsProvider.readDB.query( + GTFSProviderDbHelper.T_TRIP_IDS, + arrayOf(GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT, GTFSProviderDbHelper.T_TRIP_IDS_K_ID), + "${GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT} IN ($placeholders)", + tripIdInts.toTypedArray(), + null, + null, + null + ).use { cursor -> + cursorToStrings(cursor) + } + } + + private fun cursorToStrings(cursor: Cursor) = buildMap { + while (cursor.moveToNext()) { + try { + put( + cursor.getInt(cursor.getColumnIndexOrThrow(GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT)), + cursor.getString(cursor.getColumnIndexOrThrow(GTFSProviderDbHelper.T_TRIP_IDS_K_ID)) ?: continue + ) + } catch (e: Exception) { + MTLog.w(this@GTFSTripIdsUtils, e, "Cannot parse trip ID cursor: '$cursor'!") + } + } + } +} diff --git a/src/main/res-current/raw/current_gtfs_schedule_trip_ids b/src/main/res-current/raw/current_gtfs_schedule_trip_ids new file mode 100644 index 00000000..e69de29b diff --git a/src/main/res-next/raw/next_gtfs_schedule_trip_ids b/src/main/res-next/raw/next_gtfs_schedule_trip_ids new file mode 100644 index 00000000..e69de29b diff --git a/src/main/res/raw/gtfs_schedule_trip_ids b/src/main/res/raw/gtfs_schedule_trip_ids new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/org/mtransit/android/commons/SqlUtilsTest.kt b/src/test/java/org/mtransit/android/commons/SqlUtilsTest.kt index 91a304d9..a9142550 100644 --- a/src/test/java/org/mtransit/android/commons/SqlUtilsTest.kt +++ b/src/test/java/org/mtransit/android/commons/SqlUtilsTest.kt @@ -6,17 +6,17 @@ import org.junit.Test class SqlUtilsTest { @Test - fun test_unescapeStringOrNull() { - assertEquals(null, SqlUtils.unescapeStringOrNull("")) - assertEquals(null, SqlUtils.unescapeStringOrNull("'")) - assertEquals(null, SqlUtils.unescapeStringOrNull("''")) - assertEquals(null, SqlUtils.unescapeStringOrNull("'''")) + fun test_unquoteUnescapeStringOrNull() { + assertEquals(null, SqlUtils.unquoteUnescapeStringOrNull("")) + assertEquals(null, SqlUtils.unquoteUnescapeStringOrNull("'")) + assertEquals(null, SqlUtils.unquoteUnescapeStringOrNull("''")) + assertEquals(null, SqlUtils.unquoteUnescapeStringOrNull("'''")) // - assertEquals("abc", SqlUtils.unescapeStringOrNull("abc")) - assertEquals("abc", SqlUtils.unescapeStringOrNull("'abc'")) - assertEquals("a'bc", SqlUtils.unescapeStringOrNull("a'bc")) - assertEquals("a'bc", SqlUtils.unescapeStringOrNull("'a'bc'")) - assertEquals("a'bc", SqlUtils.unescapeStringOrNull("a''bc")) - assertEquals("a'bc", SqlUtils.unescapeStringOrNull("'a''bc'")) + assertEquals("abc", SqlUtils.unquoteUnescapeStringOrNull("abc")) + assertEquals("abc", SqlUtils.unquoteUnescapeStringOrNull("'abc'")) + assertEquals("a'bc", SqlUtils.unquoteUnescapeStringOrNull("a'bc")) + assertEquals("a'bc", SqlUtils.unquoteUnescapeStringOrNull("'a'bc'")) + assertEquals("a'bc", SqlUtils.unquoteUnescapeStringOrNull("a''bc")) + assertEquals("a'bc", SqlUtils.unquoteUnescapeStringOrNull("'a''bc'")) } } \ No newline at end of file diff --git a/src/test/java/org/mtransit/android/commons/TimeUtilsTest.java b/src/test/java/org/mtransit/android/commons/TimeUtilsTest.java index 17659e76..4c289ecf 100644 --- a/src/test/java/org/mtransit/android/commons/TimeUtilsTest.java +++ b/src/test/java/org/mtransit/android/commons/TimeUtilsTest.java @@ -12,4 +12,11 @@ public void test_timeToTheMinuteMillis() { assertEquals(1746320400_000L, TimeUtils.timeToTheMinuteMillis(1746320401_000L)); } + + @Test + public void test_formatSimpleDuration() { + assertEquals("1 days 2 h 3 min 4 sec 5 ms", TimeUtils.formatSimpleDuration(93784005)); + assertEquals("22 sec 915 ms", TimeUtils.formatSimpleDuration(22_915L)); + assertEquals("-1 min", TimeUtils.formatSimpleDuration(-60_000L)); + } } \ No newline at end of file