diff --git a/src/debug/res/values/constants.xml b/src/debug/res/values/constants.xml index 19c858ec..06e50838 100644 --- a/src/debug/res/values/constants.xml +++ b/src/debug/res/values/constants.xml @@ -10,6 +10,8 @@ org.mtransit.android.debug.provider.SCHEDULE_PROVIDER org.mtransit.android.debug.providerSCHEDULE_PROVIDER_TARGET + org.mtransit.android.debug.provider.VEHICLE_LOCATION_PROVIDER + org.mtransit.android.debug.provider.VEHICLE_LOCATION_PROVIDER_TARGET org.mtransit.android.debug.provider.SERVICE_UPDATE_PROVIDER org.mtransit.android.debug.provider.SERVICE_UPDATE_PROVIDER_TARGET org.mtransit.android.debug.provider.NEWS_PROVIDER diff --git a/src/main/java/org/mtransit/android/commons/PreferenceUtils.java b/src/main/java/org/mtransit/android/commons/PreferenceUtils.java index e73cce8a..afd3a841 100644 --- a/src/main/java/org/mtransit/android/commons/PreferenceUtils.java +++ b/src/main/java/org/mtransit/android/commons/PreferenceUtils.java @@ -94,10 +94,9 @@ public static String getPREFS_LCL_AGENCY_TYPE_TAB_AGENCY(int typeId) { } private static final String PREFS_LCL_AGENCY_LAST_OPENED = "pAgencyLastOpened"; - public static final long PREFS_LCL_AGENCY_LAST_OPENED_DEFAULT = -1L; @NonNull - public static String getPREFS_LCL_AGENCY_LAST_OPENED_DEFAULT(String authority) { + public static String getPREFS_LCL_AGENCY_LAST_OPENED_DEFAULT(@NonNull String authority) { return PREFS_LCL_AGENCY_LAST_OPENED + authority; } diff --git a/src/main/java/org/mtransit/android/commons/data/Direction.java b/src/main/java/org/mtransit/android/commons/data/Direction.java index ba253d96..a55359ab 100644 --- a/src/main/java/org/mtransit/android/commons/data/Direction.java +++ b/src/main/java/org/mtransit/android/commons/data/Direction.java @@ -22,7 +22,7 @@ import java.util.Comparator; @SuppressWarnings("WeakerAccess") -public class Direction { +public class Direction implements Targetable { private static final String LOG_TAG = Direction.class.getSimpleName(); @@ -41,6 +41,8 @@ public class Direction { public static final int HEADSIGN_TYPE_STOP_ID = 3; public static final int HEADSIGN_TYPE_NO_PICKUP = 4; + @NonNull + private final String authority; private final long id; @HeadSignType private final int headsignType; @@ -48,10 +50,13 @@ public class Direction { private final String headsignValue; private final long routeId; - public Direction(long id, - @HeadSignType int headsignType, - @NonNull String headsignValue, - long routeId) { + public Direction( + @NonNull String authority, + long id, + @HeadSignType int headsignType, + @NonNull String headsignValue, + long routeId) { + this.authority = authority; this.id = id; this.headsignType = headsignType; this.headsignValue = headsignValue; @@ -59,8 +64,9 @@ public Direction(long id, } @NonNull - public static Direction fromCursor(@NonNull Cursor c) { + public static Direction fromCursor(@NonNull Cursor c, @NonNull String authority) { return new Direction( + authority, CursorExtKt.getLong(c, GTFSProviderContract.DirectionColumns.T_DIRECTION_K_ID), CursorExtKt.getInt(c, GTFSProviderContract.DirectionColumns.T_DIRECTION_K_HEADSIGN_TYPE), CursorExtKt.getString(c, GTFSProviderContract.DirectionColumns.T_DIRECTION_K_HEADSIGN_VALUE), @@ -72,7 +78,8 @@ public static Direction fromCursor(@NonNull Cursor c) { @Override public String toString() { return Direction.class.getSimpleName() + "{" + - "id=" + id + + "authority='" + authority + '\'' + + ", id=" + id + ", headsignType=" + headsignType + ", headsignValue='" + headsignValue + '\'' + ", routeId=" + routeId + @@ -100,9 +107,10 @@ public static JSONObject toJSON(@NonNull Direction direction) { } @NonNull - public static Direction fromJSON(@NonNull JSONObject jDirection) throws JSONException { + public static Direction fromJSON(@NonNull JSONObject jDirection, @NonNull String authority) throws JSONException { try { return new Direction( + authority, jDirection.getLong(JSON_ID), jDirection.getInt(JSON_HEADSIGN_TYPE), jDirection.getString(JSON_HEADSIGN_VALUE), @@ -220,8 +228,14 @@ public static boolean isSameHeadsign(@Nullable String stringHeadsign1, @Nullable } @NonNull - public String getUUID(@NonNull String authority) { - return POI.POIUtils.getUUID(authority, this.routeId, this.id); + public String getAuthority() { + return this.authority; + } + + @NonNull + @Override + public String getUUID() { + return POI.POIUtils.getUUID(this.authority, this.routeId, this.id); } public long getId() { diff --git a/src/main/java/org/mtransit/android/commons/data/POI.java b/src/main/java/org/mtransit/android/commons/data/POI.java index 6cd68e13..40172c72 100644 --- a/src/main/java/org/mtransit/android/commons/data/POI.java +++ b/src/main/java/org/mtransit/android/commons/data/POI.java @@ -17,7 +17,7 @@ import java.lang.annotation.Retention; -public interface POI extends MTLog.Loggable { +public interface POI extends Targetable, MTLog.Loggable { @Retention(SOURCE) @IntDef({ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, ITEM_VIEW_TYPE_BASIC_POI, ITEM_VIEW_TYPE_MODULE, ITEM_VIEW_TYPE_TEXT_MESSAGE, ITEM_VIEW_TYPE_PLACE}) @@ -71,9 +71,6 @@ public interface POI extends MTLog.Loggable { boolean hasLocation(); - @NonNull - String getUUID(); - @NonNull String getAuthority(); diff --git a/src/main/java/org/mtransit/android/commons/data/Route.java b/src/main/java/org/mtransit/android/commons/data/Route.java index 500d4aa6..ba2e2bea 100644 --- a/src/main/java/org/mtransit/android/commons/data/Route.java +++ b/src/main/java/org/mtransit/android/commons/data/Route.java @@ -25,7 +25,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Route implements MTLog.Loggable { +public class Route implements Targetable, MTLog.Loggable { private static final String LOG_TAG = Route.class.getSimpleName(); @@ -210,6 +210,7 @@ public long getId() { private String uuid = null; @NonNull + @Override public String getUUID() { if (this.uuid == null) { this.uuid = POI.POIUtils.getUUID(this.authority, this.id); @@ -221,6 +222,7 @@ public void resetUUID() { this.uuid = null; } + @SuppressWarnings("unused") // main app only @NonNull public Collection getAllUUIDs() { return Arrays.asList( diff --git a/src/main/java/org/mtransit/android/commons/data/RouteDirection.java b/src/main/java/org/mtransit/android/commons/data/RouteDirection.java index 86ff32f3..edb0db84 100644 --- a/src/main/java/org/mtransit/android/commons/data/RouteDirection.java +++ b/src/main/java/org/mtransit/android/commons/data/RouteDirection.java @@ -16,7 +16,7 @@ import java.util.Arrays; import java.util.Collection; -public class RouteDirection implements MTLog.Loggable { +public class RouteDirection implements Targetable, MTLog.Loggable { private static final String LOG_TAG = RouteDirection.class.getSimpleName(); @@ -40,10 +40,12 @@ public RouteDirection( } @NonNull + @Override public String getUUID() { - return direction.getUUID(getAuthority()); + return direction.getUUID(); } + @SuppressWarnings("unused") // main app only @NonNull public Collection getAllUUIDs() { return Arrays.asList( @@ -109,7 +111,7 @@ public static RouteDirection fromJSON(@NonNull JSONObject json, @NonNull String try { return new RouteDirection( Route.fromJSON(json.getJSONObject(JSON_ROUTE), authority), - Direction.fromJSON(json.getJSONObject(JSON_DIRECTION)) + Direction.fromJSON(json.getJSONObject(JSON_DIRECTION), authority) ); } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while parsing JSON '%s'!", json); @@ -153,6 +155,7 @@ public static RouteDirection fromCursorStatic(@NonNull Cursor c, @NonNull String CursorExtKt.optInt(c, GTFSProviderContract.RouteDirectionColumns.T_ROUTE_K_TYPE, GTFSCommons.DEFAULT_ROUTE_TYPE) ), new Direction( + authority, CursorExtKt.getLong(c, GTFSProviderContract.RouteDirectionColumns.T_DIRECTION_K_ID), CursorExtKt.getInt(c, GTFSProviderContract.RouteDirectionColumns.T_DIRECTION_K_HEADSIGN_TYPE), CursorExtKt.getString(c, GTFSProviderContract.RouteDirectionColumns.T_DIRECTION_K_HEADSIGN_VALUE), 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 0030571f..a031f512 100644 --- a/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java +++ b/src/main/java/org/mtransit/android/commons/data/RouteDirectionStop.java @@ -56,11 +56,12 @@ public RouteDirectionStop( } public RouteDirectionStop( - @DataSourceType int dataSourceTypeId, - @NonNull Route route, - @NonNull Direction direction, - @NonNull Stop stop, - boolean noPickup) { + @DataSourceType int dataSourceTypeId, + @NonNull Route route, + @NonNull Direction direction, + @NonNull Stop stop, + boolean noPickup + ) { super(route.getAuthority(), -1, dataSourceTypeId, POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, POI.ITEM_STATUS_TYPE_SCHEDULE, POI.ITEM_ACTION_TYPE_ROUTE_DIRECTION_STOP); this.route = route; this.direction = direction; @@ -82,11 +83,16 @@ public int getId() { @Override public String getUUID() { if (this.uuid == null) { - this.uuid = POI.POIUtils.getUUID(getAuthority(), getRoute().getId(), getDirection().getId(), getStop().getId()); + this.uuid = makeUUID(getAuthority(), getRoute().getId(), getDirection().getId(), getStop().getId()); } return this.uuid; } + @NonNull + public static String makeUUID(@NonNull String authority, long routeId, long directionId, int stopId) { + return POI.POIUtils.getUUID(authority, routeId, directionId, stopId); + } + @Override public void resetUUID() { this.uuid = null; @@ -201,7 +207,7 @@ public static RouteDirectionStop fromJSONStatic(@NonNull JSONObject json) { final RouteDirectionStop rds = new RouteDirectionStop( // DefaultPOI.getDSTypeIdFromJSON(json),// Route.fromJSON(json.getJSONObject(JSON_ROUTE), authority), // - Direction.fromJSON(json.getJSONObject(JSON_DIRECTION)), // + Direction.fromJSON(json.getJSONObject(JSON_DIRECTION), authority), // Stop.fromJSON(json.getJSONObject(JSON_STOP)), // json.getBoolean(JSON_NO_PICKUP) // ); @@ -261,6 +267,7 @@ public static RouteDirectionStop fromCursorStatic(@NonNull Cursor c, @NonNull St CursorExtKt.optInt(c, GTFSProviderContract.RouteDirectionStopColumns.T_ROUTE_K_TYPE, GTFSCommons.DEFAULT_ROUTE_TYPE) ), new Direction( + authority, CursorExtKt.getLong(c, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_ID), CursorExtKt.getInt(c, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_HEADSIGN_TYPE), CursorExtKt.getString(c, GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_HEADSIGN_VALUE), @@ -308,9 +315,10 @@ public Direction getDirection() { @NonNull public String getRouteDirectionUUID() { - return direction.getUUID(getAuthority()); + return direction.getUUID(); } + @SuppressWarnings("unused") // main app only @NonNull public Collection getRouteDirectionAllUUIDs() { return Arrays.asList( diff --git a/src/main/java/org/mtransit/android/commons/data/ServiceUpdateKtx.kt b/src/main/java/org/mtransit/android/commons/data/ServiceUpdateKtx.kt index ee4e62a0..a913b660 100644 --- a/src/main/java/org/mtransit/android/commons/data/ServiceUpdateKtx.kt +++ b/src/main/java/org/mtransit/android/commons/data/ServiceUpdateKtx.kt @@ -24,13 +24,13 @@ fun Iterable?.isSeverityWarningInfo(): Pair { fun Iterable.distinctByOriginalId() = this.distinctBy { it.originalId ?: it.id } // keep 1st occurrence from sorted list (in *Manager) -fun ServiceUpdateProviderContract.makeServiceUpdateNoneList(targetUUID: String, sourceId: String): ArrayList = +fun ServiceUpdateProviderContract.makeServiceUpdateNoneList(targetable: Targetable, sourceId: String): ArrayList = ArrayList().apply { - add(makeServiceUpdateNone(targetUUID, sourceId)) + add(makeServiceUpdateNone(targetable.uuid, sourceId)) } -fun ServiceUpdateProviderContract.makeServiceUpdateNone(targetUUID: String, sourceId: String): ServiceUpdate { - return ServiceUpdate( +fun ServiceUpdateProviderContract.makeServiceUpdateNone(targetUUID: String, sourceId: String) = + ServiceUpdate( null, targetUUID, TimeUtils.currentTimeMillis(), @@ -43,4 +43,3 @@ fun ServiceUpdateProviderContract.makeServiceUpdateNone(targetUUID: String, sour null, getServiceUpdateLanguage(), ) -} diff --git a/src/main/java/org/mtransit/android/commons/data/Targetable.kt b/src/main/java/org/mtransit/android/commons/data/Targetable.kt new file mode 100644 index 00000000..9a4945ff --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/data/Targetable.kt @@ -0,0 +1,8 @@ +package org.mtransit.android.commons.data + +interface Targetable { + + val uUID: String + val uuid: String get() = this.uUID + +} 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 77e61a6c..3537230d 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -40,6 +40,7 @@ import org.mtransit.android.commons.data.ServiceUpdate; import org.mtransit.android.commons.data.ServiceUpdateKtxKt; import org.mtransit.android.commons.data.Stop; +import org.mtransit.android.commons.data.Targetable; import org.mtransit.android.commons.provider.agency.AgencyUtils; import org.mtransit.android.commons.provider.common.MTContentProvider; import org.mtransit.android.commons.provider.common.MTSQLiteOpenHelper; @@ -51,6 +52,11 @@ import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateCleaner; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProvider; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProviderContract; +import org.mtransit.android.commons.provider.vehiclelocations.GTFSRealTimeVehiclePositionsProvider; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationDbHelper; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProviderContract; +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation; import org.mtransit.commons.Cleaner; import org.mtransit.commons.CollectionUtils; import org.mtransit.commons.GTFSCommons; @@ -86,6 +92,7 @@ // DO NOT MOVE: referenced in modules AndroidManifest.xml @SuppressLint("Registered") public class GTFSRealTimeProvider extends MTContentProvider implements + VehicleLocationProviderContract, ServiceUpdateProviderContract { private static final String LOG_TAG = GTFSRealTimeProvider.class.getSimpleName(); @@ -102,6 +109,7 @@ public String getLogTag() { private static UriMatcher getNewUriMatcher(String authority) { UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); ServiceUpdateProvider.append(URI_MATCHER, authority); + VehicleLocationProvider.append(URI_MATCHER, authority); return URI_MATCHER; } @@ -309,13 +317,60 @@ private static String getAGENCY_SERVICE_ALERTS_URL_CACHED(@NonNull Context conte return agencyServiceAlertsUrlCached; } + @Nullable + private static String agencyVehiclesUrl = null; + + @NonNull + public static String getAgencyVehiclePositionsUrlString(@NonNull Context context, @NonNull String token) { + if (agencyVehiclesUrl == null) { + agencyVehiclesUrl = getAGENCY_VEHICLE_POSITIONS_URL(context, + token, // 1st (some agency config have only 1 "%s") + MT_HASH_SECRET_AND_DATE + ); + } + return agencyVehiclesUrl; + } + + @Nullable + private static String agencyVehiclePositionsUrl = null; + + /** + * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. + */ + @NonNull + @SuppressLint("StringFormatInvalid") // empty string: set in module app + private static String getAGENCY_VEHICLE_POSITIONS_URL( + @NonNull Context context, + @NonNull String token, + @SuppressWarnings("SameParameterValue") @NonNull String hash + ) { + if (agencyVehiclePositionsUrl == null) { + agencyVehiclePositionsUrl = context.getResources().getString(R.string.gtfs_real_time_agency_vehicle_positions_url, token, hash); + } + return agencyVehiclePositionsUrl; + } + + @Nullable + private static String agencyVehiclePositionsUrlCached = null; + + /** + * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. + */ + @NonNull + public static String getAGENCY_VEHICLE_POSITIONS_URL_CACHED(@NonNull Context context) { + if (agencyVehiclePositionsUrlCached == null) { + agencyVehiclePositionsUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_vehicle_positions_url_cached); + } + return agencyVehiclePositionsUrlCached; + } + @Nullable private static Boolean ignoreDirection = null; /** * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. */ - private static boolean isIGNORE_DIRECTION(@NonNull Context context) { + public static boolean isIGNORE_DIRECTION(@NonNull Context context) { if (ignoreDirection == null) { ignoreDirection = context.getResources().getBoolean(R.bool.gtfs_real_time_agency_ignore_direction); } @@ -446,6 +501,58 @@ private static String getAGENCY_TIME_ZONE(@NonNull Context context) { return agencyTimeZone; } + @SuppressWarnings("unused") + @Override + public long getMinDurationBetweenVehicleLocationRefreshInMs(boolean inFocus) { + return GTFSRealTimeVehiclePositionsProvider.getMinDurationBetweenRefreshInMs(this, inFocus); + } + + @Override + public long getVehicleLocationMaxValidityInMs() { + return GTFSRealTimeVehiclePositionsProvider.getMaxValidityInMs(this); + } + + @Override + public long getVehicleLocationValidityInMs(boolean inFocus) { + return GTFSRealTimeVehiclePositionsProvider.getValidityInMs(this, inFocus); + } + + @Override + public void cacheVehicleLocations(@NonNull List newVehicleLocations) { + VehicleLocationProvider.cacheVehicleLocationsS(this, newVehicleLocations); + } + + @Override + public @Nullable List getCachedVehicleLocations(@NonNull VehicleLocationProviderContract.Filter vehicleLocationFilter) { + return GTFSRealTimeVehiclePositionsProvider.getCached(this, vehicleLocationFilter); + } + + @Override + public @Nullable List getNewVehicleLocations(@NonNull VehicleLocationProviderContract.Filter vehicleLocationFilter) { + this.providedAgencyUrlToken = SecureStringUtils.dec(vehicleLocationFilter.getProvidedEncryptKey(KeysIds.GTFS_REAL_TIME_URL_TOKEN)); + this.providedAgencyUrlSecret = SecureStringUtils.dec(vehicleLocationFilter.getProvidedEncryptKey(KeysIds.GTFS_REAL_TIME_URL_SECRET)); + return GTFSRealTimeVehiclePositionsProvider.getNew(this, vehicleLocationFilter); + } + + @Override + public boolean deleteCachedVehicleLocation(int vehicleLocationId) { + return VehicleLocationProvider.deleteCachedVehicleLocation(this, vehicleLocationId); + } + + public boolean deleteAllCachedVehicleLocations() { + return VehicleLocationProvider.deleteAllCachedVehicleLocations(this); + } + + @Override + public boolean purgeUselessCachedVehicleLocations() { + return VehicleLocationProvider.purgeUselessCachedVehicleLocations(this); + } + + @Override + public @NonNull String getVehicleLocationDbTableName() { + return GTFSRealTimeDbHelper.T_GTFS_REAL_TIME_VEHICLE_LOCATION; + } + private static final long SERVICE_UPDATE_MAX_VALIDITY_IN_MS = TimeUnit.DAYS.toMillis(1L); private static final long SERVICE_UPDATE_VALIDITY_IN_MS = TimeUnit.MINUTES.toMillis(30L); private static final long SERVICE_UPDATE_VALIDITY_IN_FOCUS_IN_MS = TimeUnit.MINUTES.toMillis(1L); @@ -585,7 +692,7 @@ private Map getProviderTargetUUIDs(@NonNull Context context, @No } @NonNull - private String getAgencyTag(@NonNull Context context) { + public String getAgencyTag(@NonNull Context context) { return getRDS_AGENCY_ID(context); } @@ -600,7 +707,7 @@ private String getRouteTag(@NonNull RouteDirection rd) { } @NonNull - private String getRouteTag(@NonNull Route route) { + public String getRouteTag(@NonNull Route route) { return String.valueOf(route.getOriginalIdHash()); } @@ -630,7 +737,7 @@ private Integer getDirectionTag(@NonNull RouteDirection rd) { } @Nullable - private Integer getDirectionTag(@NonNull Direction direction) { + public Integer getDirectionTag(@NonNull Direction direction) { return direction.getOriginalDirectionIdOrNull(); } @@ -702,31 +809,27 @@ public ArrayList getNewServiceUpdates(@NonNull ServiceUpdateProvi private ArrayList getNewServiceUpdates(@NonNull RouteDirectionStop rds, boolean inFocus) { final Context context = requireContextCompat(); updateAgencyServiceUpdateDataIfRequired(context, inFocus); - final String authority = rds.getAuthority(); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rds); - return getServiceUpdatesOrNone(context, authority, cachedServiceUpdates); + return getServiceUpdatesOrNone(context, rds, cachedServiceUpdates); } private ArrayList getNewServiceUpdates(@NonNull RouteDirection rd, boolean inFocus) { final Context context = requireContextCompat(); updateAgencyServiceUpdateDataIfRequired(context, inFocus); - final String authority = rd.getAuthority(); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rd); - return getServiceUpdatesOrNone(context, authority, cachedServiceUpdates); + return getServiceUpdatesOrNone(context, rd, cachedServiceUpdates); } private ArrayList getNewServiceUpdates(@NonNull Route route, boolean inFocus) { final Context context = requireContextCompat(); updateAgencyServiceUpdateDataIfRequired(context, inFocus); - final String authority = route.getAuthority(); ArrayList cachedServiceUpdates = getCachedServiceUpdates(route); - return getServiceUpdatesOrNone(context, authority, cachedServiceUpdates); + return getServiceUpdatesOrNone(context, route, cachedServiceUpdates); } - private ArrayList getServiceUpdatesOrNone(Context context, String authority, ArrayList cachedServiceUpdates) { + private ArrayList getServiceUpdatesOrNone(Context context, Targetable target, ArrayList cachedServiceUpdates) { if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - final String agencyProviderTargetUUID = getAgencyTagTargetUUID(authority); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyProviderTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, target, AGENCY_SOURCE_ID); enhanceServiceUpdate(context, cachedServiceUpdates, Collections.emptyMap()); // convert to stop service update } return cachedServiceUpdates; @@ -822,7 +925,7 @@ private static String getAgencyServiceAlertsUrlString(@NonNull Context context, private OkHttpClient okHttpClient = null; @NonNull - private OkHttpClient getOkHttpClient(@NonNull Context context) { + public OkHttpClient getOkHttpClient(@NonNull Context context) { if (this.okHttpClient == null) { this.okHttpClient = NetworkUtils.makeNewOkHttpClientWithInterceptor(context); } @@ -1233,7 +1336,7 @@ private Pattern getServiceIdCleanupPattern(@NonNull Context context) { private boolean routeIdCleanupPatternSet = false; @Nullable - private Pattern getRouteIdCleanupPattern(@NonNull Context context) { + public Pattern getRouteIdCleanupPattern(@NonNull Context context) { if (this.routeIdCleanupPattern == null && !routeIdCleanupPatternSet) { this.routeIdCleanupPattern = GTFSCommons.makeIdCleanupPattern(getROUTE_ID_CLEANUP_REGEX(context)); this.routeIdCleanupPatternSet = true; @@ -1247,7 +1350,7 @@ private Pattern getRouteIdCleanupPattern(@NonNull Context context) { private boolean tripIdCleanupPatternSet = false; @Nullable - private Pattern getTripIdCleanupPattern(@NonNull Context context) { + public Pattern getTripIdCleanupPattern(@NonNull Context context) { if (this.tripIdCleanupPattern == null && !tripIdCleanupPatternSet) { this.tripIdCleanupPattern = GTFSCommons.makeIdCleanupPattern(getTRIP_ID_CLEANUP_REGEX(context)); this.tripIdCleanupPatternSet = true; @@ -1461,6 +1564,7 @@ public UriMatcher getURI_MATCHER() { } @NonNull + @Override public String getAuthority() { return getAUTHORITY(requireContextCompat()); } @@ -1495,6 +1599,10 @@ public Cursor queryMT(@NonNull Uri uri, @Nullable String[] projection, @Nullable if (cursor != null) { return cursor; } + cursor = VehicleLocationProvider.queryS(this, uri, selection); + if (cursor != null) { + return cursor; + } throw new IllegalArgumentException(String.format("Unknown URI (query): '%s'", uri)); } @@ -1505,6 +1613,10 @@ public String getTypeMT(@NonNull Uri uri) { if (type != null) { return type; } + type = VehicleLocationProvider.getTypeS(this, uri); + if (type != null) { + return type; + } throw new IllegalArgumentException(String.format("Unknown URI (type): '%s'", uri)); } @@ -1542,6 +1654,13 @@ public String getLogTag() { */ protected static final String DB_NAME = "gtfsrealtime.db"; + static final String T_GTFS_REAL_TIME_VEHICLE_LOCATION = VehicleLocationDbHelper.T_VEHICLE_LOCATION; + + private static final String T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_CREATE = VehicleLocationDbHelper.getSqlCreateBuilder( + T_GTFS_REAL_TIME_VEHICLE_LOCATION).build(); + + private static final String T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_DROP = SqlUtils.getSQLDropIfExistsQuery(T_GTFS_REAL_TIME_VEHICLE_LOCATION); + static final String T_GTFS_REAL_TIME_SERVICE_UPDATE = ServiceUpdateProvider.ServiceUpdateDbHelper.T_SERVICE_UPDATE; private static final String T_GTFS_REAL_TIME_SERVICE_UPDATE_SQL_CREATE = ServiceUpdateProvider.ServiceUpdateDbHelper.getSqlCreateBuilder( @@ -1558,6 +1677,9 @@ public static int getDbVersion(@NonNull Context context) { if (dbVersion < 0) { dbVersion = context.getResources().getInteger(R.integer.gtfs_real_time_db_version); dbVersion++; // add "service_update.original_id" column + dbVersion++; // add "vehicle_location" table + dbVersion++; // add "vehicle_location.report_timestamp" column + dbVersion++; // change "vehicle_location.[bearing|speed] unit to Int } return dbVersion; } @@ -1576,6 +1698,7 @@ public void onCreateMT(@NonNull SQLiteDatabase db) { @Override public void onUpgradeMT(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL(T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_DROP); db.execSQL(T_GTFS_REAL_TIME_SERVICE_UPDATE_SQL_DROP); GtfsRealTimeStorage.saveServiceUpdateLastUpdateMs(context, 0L); initAllDbTables(db); @@ -1586,6 +1709,7 @@ public boolean isDbExist(@NonNull Context context) { } private void initAllDbTables(@NonNull SQLiteDatabase db) { + db.execSQL(T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_CREATE); db.execSQL(T_GTFS_REAL_TIME_SERVICE_UPDATE_SQL_CREATE); } } 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 038bbdd1..20d8dea2 100644 --- a/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java @@ -20,7 +20,6 @@ import org.mtransit.android.commons.LocaleUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.NetworkUtils; -import org.mtransit.android.commons.PreferenceUtils; import org.mtransit.android.commons.R; import org.mtransit.android.commons.SqlUtils; import org.mtransit.android.commons.TimeUtils; @@ -40,11 +39,18 @@ import org.mtransit.android.commons.provider.agency.AgencyUtils; import org.mtransit.android.commons.provider.common.MTContentProvider; import org.mtransit.android.commons.provider.common.MTSQLiteOpenHelper; +import org.mtransit.android.commons.provider.nextbus.NextBusStorage; +import org.mtransit.android.commons.provider.nextbus.api.NextBusApi; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateCleaner; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProvider; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProviderContract; import org.mtransit.android.commons.provider.status.StatusProvider; import org.mtransit.android.commons.provider.status.StatusProviderContract; +import org.mtransit.android.commons.provider.vehiclelocations.NextBusVehicleLocationsProvider; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationDbHelper; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider; +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProviderContract; +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation; import org.mtransit.commons.CleanUtils; import org.mtransit.commons.Cleaner; import org.mtransit.commons.CollectionUtils; @@ -64,6 +70,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -74,13 +81,17 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import retrofit2.Retrofit; // https://retro.umoiq.com/xmlFeedDocs/NextBusXMLFeed.pdf // https://retro.umoiq.com/service/publicXMLFeed?command=agencyList // https://retro.umoiq.com/service/publicJSONFeed?command=agencyList // DO NOT MOVE: referenced in modules AndroidManifest.xml @SuppressLint("Registered") -public class NextBusProvider extends MTContentProvider implements ServiceUpdateProviderContract, StatusProviderContract { +public class NextBusProvider extends MTContentProvider implements + VehicleLocationProviderContract, + ServiceUpdateProviderContract, + StatusProviderContract { private static final String LOG_TAG = NextBusProvider.class.getSimpleName(); @@ -90,16 +101,12 @@ public String getLogTag() { return LOG_TAG; } - /** - * Override if multiple {@link NextBusProvider} implementations in same app. - */ - private static final String PREF_KEY_AGENCY_LAST_UPDATE_MS = NextBusDbHelper.PREF_KEY_AGENCY_LAST_UPDATE_MS; - @NonNull public static UriMatcher getNewUriMatcher(@NonNull String authority) { UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); ServiceUpdateProvider.append(URI_MATCHER, authority); StatusProvider.append(URI_MATCHER, authority); + VehicleLocationProvider.append(URI_MATCHER, authority); return URI_MATCHER; } @@ -131,20 +138,6 @@ private static String getAUTHORITY(@NonNull Context context) { return authority; } - @Nullable - private static String targetAuthority = null; - - /** - * Override if multiple {@link NextBusProvider} implementations in same app. - */ - @NonNull - private static String getTARGET_AUTHORITY(@NonNull Context context) { - if (targetAuthority == null) { - targetAuthority = context.getResources().getString(R.string.next_bus_for_poi_authority); - } - return targetAuthority; - } - @Nullable private static Uri authorityUri = null; @@ -166,7 +159,7 @@ private static Uri getAUTHORITY_URI(@NonNull Context context) { * Override if multiple {@link NextBusProvider} implementations in same app. */ @NonNull - private static String getAGENCY_TAG(@NonNull Context context) { + public static String getAGENCY_TAG(@NonNull Context context) { if (agencyTag == null) { agencyTag = context.getResources().getString(R.string.next_bus_agency_tag); } @@ -261,7 +254,7 @@ private static String getTEXT_SECONDARY_BOLD_WORDS(@NonNull Context context) { /** * Override if multiple {@link NextBusProvider} implementations in same app. */ - private static boolean isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(@NonNull Context context) { + public static boolean isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(@NonNull Context context) { if (appendHeadSignValueToRouteTag == null) { appendHeadSignValueToRouteTag = context.getResources().getBoolean(R.bool.next_bus_route_tag_append_headsign_value); } @@ -501,6 +494,77 @@ private static java.util.List getSCHEDULE_HEAD_SIGN_PREDICTIONS_ROUTE_TI return scheduleHeadSignPredictionsRouteTitleReplacement; } + @Nullable + private NextBusApi nextBusApi = null; + + @NonNull + public NextBusApi getNextBusApi(@NonNull Context context) { + if (this.nextBusApi == null) { + this.nextBusApi = createNextBusApi(context); + } + return this.nextBusApi; + } + + @NonNull + private NextBusApi createNextBusApi(@NonNull Context context) { + final Retrofit retrofit = NetworkUtils.makeNewRetrofitWithGson( + NextBusApi.BASE_HOST_URL, + context, + NetworkUtils.makeNewOkHttpClientWithInterceptor(context) + ); + return retrofit.create(NextBusApi.class); + } + + @SuppressWarnings("unused") + @Override + public long getMinDurationBetweenVehicleLocationRefreshInMs(boolean inFocus) { + return NextBusVehicleLocationsProvider.getMinDurationBetweenRefreshInMs(inFocus); + } + + @Override + public long getVehicleLocationMaxValidityInMs() { + return NextBusVehicleLocationsProvider.getMaxValidityInMs(); + } + + @Override + public long getVehicleLocationValidityInMs(boolean inFocus) { + return NextBusVehicleLocationsProvider.getValidityInMs(inFocus); + } + + @Override + public void cacheVehicleLocations(@NonNull List newVehicleLocations) { + VehicleLocationProvider.cacheVehicleLocationsS(this, newVehicleLocations); + } + + @Override + public @Nullable List getCachedVehicleLocations(@NonNull VehicleLocationProviderContract.Filter vehicleLocationFilter) { + return NextBusVehicleLocationsProvider.getCached(this, vehicleLocationFilter); + } + + @Override + public @Nullable List getNewVehicleLocations(@NonNull VehicleLocationProviderContract.Filter vehicleLocationFilter) { + return NextBusVehicleLocationsProvider.getNew(this, vehicleLocationFilter); + } + + @Override + public boolean deleteCachedVehicleLocation(int vehicleLocationId) { + return VehicleLocationProvider.deleteCachedVehicleLocation(this, vehicleLocationId); + } + + public boolean deleteAllCachedVehicleLocations() { + return VehicleLocationProvider.deleteAllCachedVehicleLocations(this); + } + + @Override + public boolean purgeUselessCachedVehicleLocations() { + return VehicleLocationProvider.purgeUselessCachedVehicleLocations(this); + } + + @Override + public @NonNull String getVehicleLocationDbTableName() { + return NextBusDbHelper.T_NEXT_BUS_VEHICLE_LOCATION; + } + private static final long SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_MS = TimeUnit.MINUTES.toMillis(10L); private static final long SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = TimeUnit.MINUTES.toMillis(1L); @@ -546,27 +610,28 @@ public void cacheServiceUpdates(@NonNull ArrayList newServiceUpda @Nullable @Override public ArrayList getCachedServiceUpdates(@NonNull ServiceUpdateProviderContract.Filter serviceUpdateFilter) { + final Context context = requireContextCompat(); if ((serviceUpdateFilter.getPoi() instanceof RouteDirectionStop)) { - return getCachedServiceUpdates((RouteDirectionStop) serviceUpdateFilter.getPoi()); + return getCachedServiceUpdates(context, (RouteDirectionStop) serviceUpdateFilter.getPoi()); } else if ((serviceUpdateFilter.getRouteDirection() != null)) { // depends on agency routeTag: Toronto TTC: YES, Laval STL: NO - return getCachedServiceUpdates(serviceUpdateFilter.getRouteDirection()); + return getCachedServiceUpdates(context, serviceUpdateFilter.getRouteDirection()); } else if ((serviceUpdateFilter.getRoute() != null)) { // depends on agency routeTag: Toronto TTC: YES, Laval STL: NO - return getCachedServiceUpdates(serviceUpdateFilter.getRoute()); + return getCachedServiceUpdates(context, serviceUpdateFilter.getRoute()); } else { MTLog.w(this, "getCachedServiceUpdates() > no service update (poi null or not RDS or no route)"); return null; } } - private ArrayList getCachedServiceUpdates(@NonNull RouteDirectionStop rds) { - final Map targetUUIDs = getServiceUpdateTargetUUIDs(rds); + private ArrayList getCachedServiceUpdates(@NonNull Context context, @NonNull RouteDirectionStop rds) { + final Map targetUUIDs = getServiceUpdateTargetUUIDs(context, rds); ArrayList cachedServiceUpdates = ServiceUpdateProvider.getCachedServiceUpdatesS(this, targetUUIDs.keySet()); enhanceRDServiceUpdateForStop(cachedServiceUpdates, targetUUIDs); return cachedServiceUpdates; } - private ArrayList getCachedServiceUpdates(@NonNull RouteDirection rd) { - final Map targetUUIDs = getServiceUpdateTargetUUIDs(rd); + private ArrayList getCachedServiceUpdates(@NonNull Context context, @NonNull RouteDirection rd) { + final Map targetUUIDs = getServiceUpdateTargetUUIDs(context, rd); ArrayList cachedServiceUpdates = ServiceUpdateProvider.getCachedServiceUpdatesS(this, targetUUIDs.keySet()); enhanceRDServiceUpdateForStop(cachedServiceUpdates, targetUUIDs); // if (org.mtransit.commons.Constants.DEBUG) { @@ -580,8 +645,8 @@ private ArrayList getCachedServiceUpdates(@NonNull RouteDirection return cachedServiceUpdates; } - private ArrayList getCachedServiceUpdates(@NonNull Route route) { - final Map targetUUIDs = getServiceUpdateTargetUUIDs(route); + private ArrayList getCachedServiceUpdates(@NonNull Context context, @NonNull Route route) { + final Map targetUUIDs = getServiceUpdateTargetUUIDs(context, route); ArrayList cachedServiceUpdates = ServiceUpdateProvider.getCachedServiceUpdatesS(this, targetUUIDs.keySet()); enhanceRDServiceUpdateForStop(cachedServiceUpdates, targetUUIDs); // if (org.mtransit.commons.Constants.DEBUG) { @@ -610,56 +675,59 @@ private void enhanceRDServiceUpdateForStop(@Nullable ArrayList se } @NonNull - private Map getServiceUpdateTargetUUIDs(@NonNull RouteDirectionStop rds) { + private Map getServiceUpdateTargetUUIDs(@NonNull Context context, @NonNull RouteDirectionStop rds) { final HashMap targetUUIDs = new HashMap<>(); - targetUUIDs.put(getServiceUpdateAgencyTargetUUID(rds.getAuthority()), rds.getAuthority()); + final String agencyTag = getAGENCY_TAG(context); + targetUUIDs.put(getAgencyTargetUUID(agencyTag), rds.getAuthority()); if (!isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(requireContextCompat())) { - targetUUIDs.put(getServiceUpdateAgencyRouteTagTargetUUID(rds.getAuthority(), getRouteTag(rds.getRoute(), null)), rds.getRoute().getUUID()); + targetUUIDs.put(getAgencyRouteTagTargetUUID(agencyTag, getRouteTag(rds.getRoute(), null)), rds.getRoute().getUUID()); } else { // STLaval - targetUUIDs.put(getServiceUpdateAgencyRouteTagTargetUUID(rds.getAuthority(), getRouteTag(rds)), rds.getRouteDirectionUUID()); + targetUUIDs.put(getAgencyRouteTagTargetUUID(agencyTag, getRouteTag(rds)), rds.getRouteDirectionUUID()); } - targetUUIDs.put(getAgencyRouteStopTagTargetUUID(rds), rds.getUUID()); + targetUUIDs.put(getAgencyRouteStopTagTargetUUID(context, rds), rds.getUUID()); return targetUUIDs; } @NonNull - private Map getServiceUpdateTargetUUIDs(@NonNull RouteDirection rd) { + private Map getServiceUpdateTargetUUIDs(@NonNull Context context, @NonNull RouteDirection rd) { final HashMap targetUUIDs = new HashMap<>(); - targetUUIDs.put(getServiceUpdateAgencyTargetUUID(rd.getAuthority()), rd.getAuthority()); - if (!isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(requireContextCompat())) { - targetUUIDs.put(getServiceUpdateAgencyRouteTagTargetUUID(rd.getAuthority(), getRouteTag(rd.getRoute(), null)), rd.getRoute().getUUID()); + final String agencyTag = getAGENCY_TAG(context); + targetUUIDs.put(getAgencyTargetUUID(agencyTag), rd.getAuthority()); + if (!isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(context)) { + targetUUIDs.put(getAgencyRouteTagTargetUUID(agencyTag, getRouteTag(rd.getRoute(), null)), rd.getRoute().getUUID()); } else { // STLaval - targetUUIDs.put(getServiceUpdateAgencyRouteTagTargetUUID(rd.getAuthority(), getRouteTag(rd)), rd.getUUID()); + targetUUIDs.put(getAgencyRouteTagTargetUUID(agencyTag, getRouteTag(rd)), rd.getUUID()); } return targetUUIDs; } @NonNull - private Map getServiceUpdateTargetUUIDs(@NonNull Route route) { + private Map getServiceUpdateTargetUUIDs(@NonNull Context context, @NonNull Route route) { final HashMap targetUUIDs = new HashMap<>(); - targetUUIDs.put(getServiceUpdateAgencyTargetUUID(route.getAuthority()), route.getAuthority()); + final String agencyTag = getAGENCY_TAG(context); + targetUUIDs.put(getAgencyTargetUUID(agencyTag), route.getAuthority()); if (!isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(requireContextCompat())) { - targetUUIDs.put(getServiceUpdateAgencyRouteTagTargetUUID(route.getAuthority(), getRouteTag(route, null)), route.getUUID()); + targetUUIDs.put(getAgencyRouteTagTargetUUID(agencyTag, getRouteTag(route, null)), route.getUUID()); } // ELSE // STLaval return targetUUIDs; } - private String getAgencyRouteStopTagTargetUUID(@NonNull RouteDirectionStop rds) { - return getAgencyRouteStopTagTargetUUID(rds.getAuthority(), getRouteTag(rds), getStopTag(rds)); + private String getAgencyRouteStopTagTargetUUID(@NonNull Context context, @NonNull RouteDirectionStop rds) { + return getAgencyRouteStopTagTargetUUID(getAGENCY_TAG(context), getRouteTag(rds), getStopTag(rds)); } @NonNull - private String getRouteTag(@NonNull RouteDirectionStop rds) { + public String getRouteTag(@NonNull RouteDirectionStop rds) { return getRouteTag(rds.getRoute(), rds.getDirection()); } @NonNull - private String getRouteTag(@NonNull RouteDirection rd) { + public String getRouteTag(@NonNull RouteDirection rd) { return getRouteTag(rd.getRoute(), rd.getDirection()); } @NonNull - private String getRouteTag(@NonNull Route route, @Nullable Direction direction) { + public String getRouteTag(@NonNull Route route, @Nullable Direction direction) { final StringBuilder sb = new StringBuilder(); sb.append(route.getShortName()); final Context context = requireContextCompat(); @@ -718,18 +786,18 @@ private String cleanStopTag(@NonNull String stopTag) { } @NonNull - protected static String getServiceUpdateAgencyRouteTagTargetUUID(@NonNull String agencyAuthority, @NonNull String routeTag) { - return POI.POIUtils.getUUID(agencyAuthority, routeTag); + public static String getAgencyRouteStopTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag, @NonNull String stopTag) { + return POI.POIUtils.getUUID(agencyTag, routeTag, stopTag); } @NonNull - protected static String getAgencyRouteStopTagTargetUUID(@NonNull String agencyAuthority, @NonNull String routeTag, @NonNull String stopTag) { - return POI.POIUtils.getUUID(agencyAuthority, routeTag, stopTag); + public static String getAgencyRouteTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag) { + return POI.POIUtils.getUUID(agencyTag, routeTag); } @NonNull - protected static String getServiceUpdateAgencyTargetUUID(@NonNull String agencyAuthority) { - return POI.POIUtils.getUUID(agencyAuthority); + public static String getAgencyTargetUUID(@NonNull String agencyTag) { + return POI.POIUtils.getUUID(agencyTag); } @Override @@ -750,43 +818,44 @@ public boolean deleteCachedServiceUpdate(@NonNull String targetUUID, @NonNull St @Nullable @Override public ArrayList getNewServiceUpdates(@NonNull ServiceUpdateProviderContract.Filter serviceUpdateFilter) { + final Context context = requireContextCompat(); if ((serviceUpdateFilter.getPoi() instanceof RouteDirectionStop)) { - return getNewServiceUpdates((RouteDirectionStop) serviceUpdateFilter.getPoi(), serviceUpdateFilter.isInFocusOrDefault()); + return getNewServiceUpdates(context, (RouteDirectionStop) serviceUpdateFilter.getPoi(), serviceUpdateFilter.isInFocusOrDefault()); } else if ((serviceUpdateFilter.getRouteDirection() != null)) { // depends on agency routeTag: Toronto TTC: YES, Laval STL: NO - return getNewServiceUpdates(serviceUpdateFilter.getRouteDirection(), serviceUpdateFilter.isInFocusOrDefault()); + return getNewServiceUpdates(context, serviceUpdateFilter.getRouteDirection(), serviceUpdateFilter.isInFocusOrDefault()); } else if ((serviceUpdateFilter.getRoute() != null)) { // depends on agency routeTag: Toronto TTC: YES, Laval STL: NO - return getNewServiceUpdates(serviceUpdateFilter.getRoute(), serviceUpdateFilter.isInFocusOrDefault()); + return getNewServiceUpdates(context, serviceUpdateFilter.getRoute(), serviceUpdateFilter.isInFocusOrDefault()); } else { MTLog.w(this, "getNewServiceUpdates() > no service update (poi null or not RDS or no route)"); return null; } } - private ArrayList getNewServiceUpdates(@NonNull RouteDirectionStop rds, boolean inFocus) { + private ArrayList getNewServiceUpdates(@NonNull Context context, @NonNull RouteDirectionStop rds, boolean inFocus) { updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), inFocus); - ArrayList cachedServiceUpdates = getCachedServiceUpdates(rds); + ArrayList cachedServiceUpdates = getCachedServiceUpdates(context, rds); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - cachedServiceUpdates = makeServiceUpdateNoneList(this, getServiceUpdateAgencyTargetUUID(rds.getAuthority()), AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rds, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, Collections.emptyMap()); } return cachedServiceUpdates; } - private ArrayList getNewServiceUpdates(@NonNull RouteDirection rd, boolean inFocus) { + private ArrayList getNewServiceUpdates(@NonNull Context context, @NonNull RouteDirection rd, boolean inFocus) { updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), inFocus); - ArrayList cachedServiceUpdates = getCachedServiceUpdates(rd); + ArrayList cachedServiceUpdates = getCachedServiceUpdates(context, rd); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - cachedServiceUpdates = makeServiceUpdateNoneList(this, getServiceUpdateAgencyTargetUUID(rd.getAuthority()), AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rd, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, Collections.emptyMap()); } return cachedServiceUpdates; } - private ArrayList getNewServiceUpdates(@NonNull Route route, boolean inFocus) { + private ArrayList getNewServiceUpdates(@NonNull Context context, @NonNull Route route, boolean inFocus) { updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), inFocus); - ArrayList cachedServiceUpdates = getCachedServiceUpdates(route); + ArrayList cachedServiceUpdates = getCachedServiceUpdates(context, route); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - cachedServiceUpdates = makeServiceUpdateNoneList(this, getServiceUpdateAgencyTargetUUID(route.getAuthority()), AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, route, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, Collections.emptyMap()); } return cachedServiceUpdates; @@ -795,7 +864,7 @@ private ArrayList getNewServiceUpdates(@NonNull Route route, bool private static final String AGENCY_SOURCE_ID = "next_bus_com_messages"; private void updateAgencyServiceUpdateDataIfRequired(@NonNull Context context, boolean inFocus) { - long lastUpdateInMs = PreferenceUtils.getPrefLcl(context, PREF_KEY_AGENCY_LAST_UPDATE_MS, 0L); + long lastUpdateInMs = NextBusStorage.getServiceUpdateLastUpdateMs(context, 0L); long minUpdateMs = Math.min(getServiceUpdateMaxValidityInMs(), getServiceUpdateValidityInMs(inFocus)); long nowInMs = TimeUtils.currentTimeMillis(); if (lastUpdateInMs + minUpdateMs > nowInMs) { @@ -805,7 +874,7 @@ private void updateAgencyServiceUpdateDataIfRequired(@NonNull Context context, b } private synchronized void updateAgencyServiceUpdateDataIfRequiredSync(@NonNull Context context, long lastUpdateInMs, boolean inFocus) { - if (PreferenceUtils.getPrefLcl(context, PREF_KEY_AGENCY_LAST_UPDATE_MS, 0L) > lastUpdateInMs) { + if (NextBusStorage.getServiceUpdateLastUpdateMs(context, 0L) > lastUpdateInMs) { return; // too late, another thread already updated } long nowInMs = TimeUtils.currentTimeMillis(); @@ -833,7 +902,7 @@ private void updateAllAgencyServiceUpdateDataFromWWW(@NonNull Context context, b deleteAllAgencyServiceUpdateData(); } cacheServiceUpdates(newServiceUpdates); - PreferenceUtils.savePrefLclSync(context, PREF_KEY_AGENCY_LAST_UPDATE_MS, nowInMs); + NextBusStorage.saveServiceUpdateLastUpdateMs(context, nowInMs); } // else keep whatever we have until max validity reached } @@ -876,7 +945,6 @@ private ArrayList loadAgencyServiceUpdateDataFromWWW(@NonNull Con sourceLabel, newLastUpdateInMs, getAGENCY_TAG(context), - getTARGET_AUTHORITY(context), getServiceUpdateMaxValidityInMs(), getTEXT_LANGUAGE_CODE(context), getTEXT_SECONDARY_LANGUAGE_CODE(context), @@ -981,15 +1049,15 @@ public POIStatus getCachedStatus(@NonNull StatusProviderContract.Filter statusFi MTLog.w(this, "getNewStatus() > Can't find new schedule w/o schedule filter!"); return null; } - Schedule.ScheduleStatusFilter scheduleStatusFilter = (Schedule.ScheduleStatusFilter) statusFilter; - RouteDirectionStop rds = scheduleStatusFilter.getRouteDirectionStop(); - String targetUUID = getAgencyRouteStopTagTargetUUID(rds); - POIStatus cachedStatus = StatusProvider.getCachedStatusS(this, targetUUID); + final Schedule.ScheduleStatusFilter scheduleStatusFilter = (Schedule.ScheduleStatusFilter) statusFilter; + final RouteDirectionStop rds = scheduleStatusFilter.getRouteDirectionStop(); + final String targetUUID = getAgencyRouteStopTagTargetUUID(requireContextCompat(), rds); + final POIStatus cachedStatus = StatusProvider.getCachedStatusS(this, targetUUID); if (cachedStatus != null) { cachedStatus.setTargetUUID(rds.getUUID()); // target RDS UUID instead of custom NextBus Route & Stop tags if (rds.isNoPickup()) { if (cachedStatus instanceof Schedule) { - Schedule schedule = (Schedule) cachedStatus; + final Schedule schedule = (Schedule) cachedStatus; schedule.setNoPickup(true); // API doesn't know about "descent only" } } @@ -1141,6 +1209,12 @@ public UriMatcher getURI_MATCHER() { return getURIMATCHER(requireContextCompat()); } + @NonNull + @Override + public String getAuthority() { + return getAUTHORITY(requireContextCompat()); + } + @NonNull @Override public Uri getAuthorityUri() { @@ -1175,6 +1249,10 @@ public Cursor queryMT(@NonNull Uri uri, @Nullable String[] projection, @Nullable if (cursor != null) { return cursor; } + cursor = VehicleLocationProvider.queryS(this, uri, selection); + if (cursor != null) { + return cursor; + } throw new IllegalArgumentException(String.format("Unknown URI (query): '%s'", uri)); } @@ -1189,6 +1267,10 @@ public String getTypeMT(@NonNull Uri uri) { if (type != null) { return type; } + type = VehicleLocationProvider.getTypeS(this, uri); + if (type != null) { + return type; + } throw new IllegalArgumentException(String.format("Unknown URI (type): '%s'", uri)); } @@ -1255,7 +1337,7 @@ public String getLogTag() { private final HashMap statuses = new HashMap<>(); private final NextBusProvider provider; - private final String authority; + private final String agencyTag; @Nullable private final String sourceLabel; private final long lastUpdateInMs; @@ -1264,7 +1346,7 @@ public String getLogTag() { NextBusPredictionsDataHandler(@NonNull NextBusProvider provider, @Nullable String sourceLabel, long lastUpdateInMs, @NonNull String localTimeZoneId) { this.provider = provider; - this.authority = NextBusProvider.getTARGET_AUTHORITY(this.provider.requireContextCompat()); + this.agencyTag = NextBusProvider.getAGENCY_TAG(this.provider.requireContextCompat()); this.sourceLabel = sourceLabel; this.lastUpdateInMs = lastUpdateInMs; this.localTimeZoneId = localTimeZoneId; @@ -1348,7 +1430,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc if (TextUtils.isEmpty(this.currentRouteTag) || TextUtils.isEmpty(this.currentStopTag)) { return; } - String targetUUID = NextBusProvider.getAgencyRouteStopTagTargetUUID(this.authority, this.currentRouteTag, this.currentStopTag); + String targetUUID = NextBusProvider.getAgencyRouteStopTagTargetUUID(this.agencyTag, this.currentRouteTag, this.currentStopTag); Schedule status = this.statuses.get(targetUUID); if (status == null) { status = new Schedule( @@ -1387,8 +1469,10 @@ private String cleanTripHeadSign(String tripHeadSign) { if (isSCHEDULE_HEAD_SIGN_CLEAN_STREET_TYPES(context)) { tripHeadSign = CleanUtils.cleanStreetTypes(tripHeadSign); } + Locale locale = Locale.ENGLISH; if (isSCHEDULE_HEAD_SIGN_CLEAN_STREET_TYPES_FR_CA(context)) { tripHeadSign = CleanUtils.cleanStreetTypesFRCA(tripHeadSign); + locale = Locale.FRENCH; } for (int c = 0; c < getSCHEDULE_HEAD_SIGN_CLEAN_REGEX(context).size(); c++) { try { @@ -1404,7 +1488,7 @@ private String cleanTripHeadSign(String tripHeadSign) { tripHeadSign = CleanUtils.CLEAN_AT.matcher(tripHeadSign).replaceAll(CleanUtils.CLEAN_AT_REPLACEMENT); tripHeadSign = CleanUtils.CLEAN_AND.matcher(tripHeadSign).replaceAll(CleanUtils.CLEAN_AND_REPLACEMENT); tripHeadSign = CleanUtils.CLEAN_ET.matcher(tripHeadSign).replaceAll(CleanUtils.CLEAN_ET_REPLACEMENT); - tripHeadSign = CleanUtils.cleanLabel(tripHeadSign); + tripHeadSign = CleanUtils.cleanLabel(locale, tripHeadSign); return tripHeadSign; } catch (Exception e) { MTLog.w(this, e, "Error while cleaning trip head sign '%s'!", tripHeadSign); @@ -1529,7 +1613,6 @@ public String getLogTag() { private final ArrayList serviceUpdates = new ArrayList<>(); private final String agencyTag; - private final String authority; private String currentRouteTag = null; @@ -1562,7 +1645,7 @@ public String getLogTag() { private final NextBusProvider provider; NextBusMessagesDataHandler(NextBusProvider provider, @NonNull String sourceLabel, long newLastUpdateInMs, - String agencyTag, String authority, + String agencyTag, long serviceUpdateMaxValidityInMs, String textLanguageCode, String textSecondaryLanguageCode, String textBoldWordsRegex, String textSecondaryBoldWordsRegex @@ -1571,7 +1654,6 @@ public String getLogTag() { this.sourceLabel = sourceLabel; this.newLastUpdateInMs = newLastUpdateInMs; this.agencyTag = agencyTag; - this.authority = authority; this.serviceUpdateMaxValidityInMs = serviceUpdateMaxValidityInMs; this.textLanguageCode = textLanguageCode; this.textSecondaryLanguageCode = textSecondaryLanguageCode; @@ -1711,14 +1793,14 @@ public void endElement(String uri, String localName, String qName) throws SAXExc final HashSet currentRouteConfiguredForMessageRoute = this.currentRouteConfiguredForMessage.get(routeTag); final int stopCount = currentRouteConfiguredForMessageRoute == null ? 0 : currentRouteConfiguredForMessageRoute.size(); if (stopCount == 0) { - final String routeTargetUUID = NextBusProvider.getServiceUpdateAgencyRouteTagTargetUUID(this.authority, routeTag); + final String routeTargetUUID = NextBusProvider.getAgencyRouteTagTargetUUID(this.agencyTag, routeTag); final int severity = findRouteSeverity(); //noinspection UnnecessaryLocalVariable final String title = routeTag; addServiceUpdates(routeTargetUUID, severity, title); } else { for (String stopTag : currentRouteConfiguredForMessageRoute) { - final String routeStopTargetUUID = NextBusProvider.getAgencyRouteStopTagTargetUUID(this.authority, routeTag, stopTag); + final String routeStopTargetUUID = NextBusProvider.getAgencyRouteStopTagTargetUUID(this.agencyTag, routeTag, stopTag); final String title = stopCount < 10 ? this.currentStopTabAndTitle.getOrDefault(stopTag, stopTag) : routeTag; @@ -1726,7 +1808,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc addServiceUpdates(routeStopTargetUUID, severity, title); } // ADD duplicates for routeTag (UI will only show it once) - final String routeTargetUUID = NextBusProvider.getServiceUpdateAgencyRouteTagTargetUUID(this.authority, routeTag); + final String routeTargetUUID = NextBusProvider.getAgencyRouteTagTargetUUID(this.agencyTag, routeTag); final int severity = ServiceUpdate.SEVERITY_INFO_RELATED_POI; //noinspection UnnecessaryLocalVariable final String title = routeTag; @@ -1734,12 +1816,12 @@ public void endElement(String uri, String localName, String qName) throws SAXExc } } } else if (this.currentRouteTag != null) { - final String routeTargetUUID = NextBusProvider.getServiceUpdateAgencyRouteTagTargetUUID(this.authority, this.currentRouteTag); + final String routeTargetUUID = NextBusProvider.getAgencyRouteTagTargetUUID(this.agencyTag, this.currentRouteTag); final String title = this.currentRouteTag; final int severity = findAgencySeverity(); addServiceUpdates(routeTargetUUID, severity, title); } else if (this.currentRouteAll) { // AGENCY - final String agencyTargetUUID = NextBusProvider.getServiceUpdateAgencyTargetUUID(this.authority); + final String agencyTargetUUID = NextBusProvider.getAgencyTargetUUID(this.agencyTag); final String title = this.agencyTag.toUpperCase(Locale.ROOT); final int severity = findAgencySeverity(); addServiceUpdates(agencyTargetUUID, severity, title); @@ -1877,10 +1959,12 @@ public String getLogTag() { */ protected static final String DB_NAME = "nextbus.db"; - /** - * Override if multiple {@link NextBusDbHelper} implementations in same app. - */ - static final String PREF_KEY_AGENCY_LAST_UPDATE_MS = "pNextBusMessagesLastUpdate"; + static final String T_NEXT_BUS_VEHICLE_LOCATION = VehicleLocationDbHelper.T_VEHICLE_LOCATION; + + private static final String T_NEXT_BUS_VEHICLE_LOCATION_SQL_CREATE = VehicleLocationDbHelper.getSqlCreateBuilder( + T_NEXT_BUS_VEHICLE_LOCATION).build(); + + private static final String T_NEXT_BUS_VEHICLE_LOCATION_SQL_DROP = SqlUtils.getSQLDropIfExistsQuery(T_NEXT_BUS_VEHICLE_LOCATION); static final String T_NEXT_BUS_SERVICE_UPDATE = ServiceUpdateProvider.ServiceUpdateDbHelper.T_SERVICE_UPDATE; @@ -1904,6 +1988,7 @@ public static int getDbVersion(@NonNull Context context) { if (dbVersion < 0) { dbVersion = context.getResources().getInteger(R.integer.next_bus_db_version); dbVersion++; // add "service_update.original_id" column + dbVersion++; // add "vehicle_location" table } return dbVersion; } @@ -1925,7 +2010,8 @@ public void onCreateMT(@NonNull SQLiteDatabase db) { public void onUpgradeMT(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(T_NEXT_BUS_SERVICE_UPDATE_SQL_DROP); db.execSQL(T_NEXT_BUS_STATUS_SQL_DROP); - PreferenceUtils.savePrefLclSync(this.context, PREF_KEY_AGENCY_LAST_UPDATE_MS, 0L); + db.execSQL(T_NEXT_BUS_VEHICLE_LOCATION_SQL_DROP); + NextBusStorage.saveServiceUpdateLastUpdateMs(context, 0L); initAllDbTables(db); } @@ -1936,6 +2022,7 @@ public boolean isDbExist(@NonNull Context context) { private void initAllDbTables(@NonNull SQLiteDatabase db) { db.execSQL(T_NEXT_BUS_SERVICE_UPDATE_SQL_CREATE); db.execSQL(T_NEXT_BUS_STATUS_SQL_CREATE); + db.execSQL(T_NEXT_BUS_VEHICLE_LOCATION_SQL_CREATE); } } } diff --git a/src/main/java/org/mtransit/android/commons/provider/OCTranspoProvider.java b/src/main/java/org/mtransit/android/commons/provider/OCTranspoProvider.java index 489936c3..547f78c4 100644 --- a/src/main/java/org/mtransit/android/commons/provider/OCTranspoProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/OCTranspoProvider.java @@ -722,8 +722,7 @@ private ArrayList getNewServiceUpdates(@NonNull RouteDirectionSto updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), rds.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rds); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = getAgencyTargetUUID(rds.getAuthority()); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rds, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, rds.getStop(), Collections.emptyMap()); } return cachedServiceUpdates; @@ -733,8 +732,7 @@ private ArrayList getNewServiceUpdates(@NonNull RouteDirection rd updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), rd.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rd); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = getAgencyTargetUUID(rd.getAuthority()); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rd, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, null, Collections.emptyMap()); } return cachedServiceUpdates; @@ -744,8 +742,7 @@ private ArrayList getNewServiceUpdates(@NonNull Route route, bool updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), route.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(route); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = getAgencyTargetUUID(route.getAuthority()); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, route, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, null, Collections.emptyMap()); } return cachedServiceUpdates; 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 c3392aaf..f4c041b1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/StmInfoApiProvider.java @@ -262,7 +262,7 @@ public ArrayList getCachedServiceUpdates(@NonNull ServiceUpdatePr } else if (serviceUpdateFilter.getRouteDirection() != null) { return getCachedServiceUpdates(context, serviceUpdateFilter.getRouteDirection()); } else if ((serviceUpdateFilter.getRoute() != null)) { // NOT SUPPORTED - return makeServiceUpdateNoneList(this, serviceUpdateFilter.getRoute().getUUID(), SERVICE_UPDATE_SOURCE_ID); + return makeServiceUpdateNoneList(this, serviceUpdateFilter.getRoute(), SERVICE_UPDATE_SOURCE_ID); } else { MTLog.w(this, "getCachedServiceUpdates() > no service update (poi null or not RDS or no route)"); return null; @@ -571,7 +571,7 @@ public ArrayList getNewServiceUpdates(@NonNull ServiceUpdateProvi } else if (serviceUpdateFilter.getRouteDirection() != null) { return getNewServiceUpdates(context, serviceUpdateFilter.getRouteDirection()); } else if ((serviceUpdateFilter.getRoute() != null)) { // NOT SUPPORTED - return makeServiceUpdateNoneList(this, serviceUpdateFilter.getRoute().getUUID(), SERVICE_UPDATE_SOURCE_ID); + return makeServiceUpdateNoneList(this, serviceUpdateFilter.getRoute(), SERVICE_UPDATE_SOURCE_ID); } else { MTLog.w(this, "getNewServiceUpdates() > no service update (poi null or not RDS or no route)"); return null; diff --git a/src/main/java/org/mtransit/android/commons/provider/StmInfoSubwayProvider.java b/src/main/java/org/mtransit/android/commons/provider/StmInfoSubwayProvider.java index 0ed3ab70..14afcf10 100644 --- a/src/main/java/org/mtransit/android/commons/provider/StmInfoSubwayProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/StmInfoSubwayProvider.java @@ -328,8 +328,7 @@ private ArrayList getNewServiceUpdates(@NonNull RouteDirectionSto updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), rds.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rds); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = rds.getUUID(); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rds, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, rds.getRoute(), Collections.emptyMap()); // convert to stop service update } return cachedServiceUpdates; @@ -340,8 +339,7 @@ private ArrayList getNewServiceUpdates(@NonNull RouteDirection rd updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), rd.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(rd); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = rd.getUUID(); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, rd, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, rd.getRoute(), Collections.emptyMap()); // convert to stop service update } return cachedServiceUpdates; @@ -352,8 +350,7 @@ private ArrayList getNewServiceUpdates(@NonNull Route route, bool updateAgencyServiceUpdateDataIfRequired(requireContextCompat(), route.getAuthority(), inFocus); ArrayList cachedServiceUpdates = getCachedServiceUpdates(route); if (CollectionUtils.getSize(cachedServiceUpdates) == 0) { - String agencyTargetUUID = route.getUUID(); - cachedServiceUpdates = makeServiceUpdateNoneList(this, agencyTargetUUID, AGENCY_SOURCE_ID); + cachedServiceUpdates = makeServiceUpdateNoneList(this, route, AGENCY_SOURCE_ID); enhanceRDServiceUpdateForStop(cachedServiceUpdates, route, Collections.emptyMap()); // convert to stop service update } return cachedServiceUpdates; 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 56a6cc4e..9790167f 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 @@ -40,5 +40,7 @@ public final class ContentProviderConstants { // public static final int NEWS = 115; // + public static final int VEHICLE_LOCATION = 118; + // public static final int ALL = 999; } 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 c4af435f..ec1063b3 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 @@ -25,8 +25,6 @@ import java.util.HashMap; import java.util.Map; -import kotlin.Unit; - public class GTFSProviderDbHelper extends MTSQLiteOpenHelper { private static final String LOG_TAG = GTFSProviderDbHelper.class.getSimpleName(); @@ -215,7 +213,7 @@ private void initAllDbTables(@NonNull SQLiteDatabase db, boolean upgrade) { initDbTableWithRetry(context, db, T_STRINGS, T_STRINGS_SQL_CREATE, T_STRINGS_SQL_INSERT, T_STRINGS_SQL_DROP, getStringsFiles(), 0, 0, null, null, (id, string) -> { allStrings.put(id, string); - return Unit.INSTANCE; + return kotlin.Unit.INSTANCE; } ); // 1st } 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 428b3e90..dd5f78f0 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 @@ -26,7 +26,7 @@ fun GTFSRealTimeProvider.makeRequest(context: Context, urlCachedString: String = } val url = URL(urlString) MTLog.i(this, "Loading from '%s'...", url.host) - MTLog.d(this, "Using token '%s' (length: %d)", if (!token.isEmpty()) "***" else "(none)", token.length) + MTLog.d(this, "Using token '%s' (length: %d)", if (token.isEmpty()) "(none)" else "***", token.length) return Request.Builder() .url(url) .apply { diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt index c3a8a4ff..9cc08c55 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt @@ -6,6 +6,42 @@ import org.mtransit.android.commons.PreferenceUtils object GtfsRealTimeStorage { + // region Vehicle location + + /** + * Override if multiple {@link GTFSRealTimeDbHelper} implementations in same app. + */ + private const val PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS = "pGTFSRealTimeVehicleLocationsLastUpdate" + + @JvmStatic + @WorkerThread + fun getVehicleLocationLastUpdateMs(context: Context, default: Long) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS, default) + + @JvmStatic + @WorkerThread + fun saveVehicleLocationLastUpdateMs(context: Context, lastUpdateInMs: Long) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS, lastUpdateInMs) + } + + /** + * Override if multiple {@link GTFSRealTimeDbHelper} implementations in same app. + */ + private const val PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE = "pGTFSRealTimeVehicleLocationLastUpdateCode" + + @JvmStatic + @WorkerThread + fun getVehicleLocationLastUpdateCode(context: Context, default: Int) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE, default) + + @JvmStatic + @WorkerThread + fun saveVehicleLocationLastUpdateCode(context: Context, code: Int) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE, code) + } + + // endregion + // region Service alerts /** diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index 8ec73ff4..4d562b43 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -22,6 +22,26 @@ object GtfsRealtimeExt { } } + @JvmStatic + fun List.toVehicles(): List = + this.filter { it.hasVehicle() }.map { it.vehicle }.distinct() + + @JvmStatic + fun List.toVehiclesWithIdPair(): List> = + this.filter { it.hasVehicle() }.map { it.vehicle to it.id }.distinctBy { it.first } + + @JvmStatic + fun List.sortVehicles(nowMs: Long = TimeUtils.currentTimeMillis()): List = + this.sortedBy { vehiclePosition -> + vehiclePosition.timestamp + } + + @JvmStatic + fun List>.sortVehiclesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = + this.sortedBy { (vehiclePosition, _) -> + vehiclePosition.timestamp + } + @JvmStatic fun List.toAlerts(): List = this.filter { it.hasAlert() }.map { it.alert }.distinct() @@ -98,6 +118,63 @@ object GtfsRealtimeExt { fun GtfsRealtime.TimeRange.endMs(): Long? = this.end.takeIf { this.hasEnd() }?.secToMs() + @JvmStatic + @JvmOverloads + fun GtfsRealtime.VehiclePosition.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { + append("VehiclePosition:") + append("{") + if (hasTrip()) append(trip.toStringExt(short = true)).append(", ") + if (hasPosition()) append(position.toStringExt(short = true)).append(", ") + if (hasVehicle()) append(vehicle.toStringExt(short = true)).append(", ") + if (hasCurrentStopSequence()) append("currentStopSequence=").append(currentStopSequence).append(", ") + if (hasCurrentStatus()) append("currentStatus=").append(currentStatus).append(", ") + if (hasStopId()) append("stopId=").append(stopId).append(", ") + if (hasTimestamp()) append("timestamp=").append(timestamp).append(", ") + if (hasOccupancyPercentage()) append("occupancyPct=").append(occupancyPercentage).append(", ") + if (hasOccupancyStatus()) append("occupancyStatus=").append(occupancyStatus).append(", ") + if (hasCongestionLevel()) append("congestionLevel=").append(congestionLevel).append(", ") + append("}") + } + + val GtfsRealtime.VehiclePosition.optTrip get() = if (hasTrip()) trip else null + val GtfsRealtime.VehiclePosition.optTimestamp get() = if (hasTimestamp()) timestamp else null + val GtfsRealtime.VehiclePosition.optPosition get() = if (hasPosition()) position else null + val GtfsRealtime.VehiclePosition.optVehicle get() = if (hasVehicle()) vehicle else null + + @JvmStatic + @JvmOverloads + fun GtfsRealtime.Position.toStringExt(short: Boolean = false) = buildString { + append(if (short) "P:" else "Position:") + append("{") + if (hasLatitude()) append("lat=").append(latitude).append(", ") + if (hasLongitude()) append("lon=").append(longitude).append(", ") + if (hasBearing()) append("bearing=").append(bearing).append(", ") + if (hasSpeed()) append("speed=").append(speed).append(", ") + if (hasOdometer()) append("odometer=").append(odometer).append(", ") + append("}") + } + + val GtfsRealtime.Position.optLatitude get() = if (hasLatitude()) latitude else null + val GtfsRealtime.Position.optLongitude get() = if (hasLongitude()) longitude else null + val GtfsRealtime.Position.optBearing get() = if (hasBearing()) bearing else null + val GtfsRealtime.Position.optSpeed get() = if (hasSpeed()) speed else null + val GtfsRealtime.Position.optOdometer get() = if (hasOdometer()) odometer else null + + @JvmStatic + @JvmOverloads + fun GtfsRealtime.VehicleDescriptor.toStringExt(short: Boolean = false) = buildString { + append(if (short) "VD:" else "VehicleDescriptor:") + append("{") + if (hasId()) append("id=").append(id).append(", ") + if (hasLabel()) append("lbl=").append(label).append(", ") + if (hasLicensePlate()) append("licensePlate=").append(licensePlate).append(", ") + if (hasWheelchairAccessible()) append("a18n=").append(wheelchairAccessible).append(", ") + append("}") + } + + val GtfsRealtime.VehicleDescriptor.optId get() = if (hasId()) id else null + val GtfsRealtime.VehicleDescriptor.optLabel get() = if (hasLabel()) label else null + @JvmStatic @JvmOverloads fun GtfsRealtime.Alert.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { @@ -188,6 +265,8 @@ object GtfsRealtimeExt { append("}") } + val GtfsRealtime.TripDescriptor.optTripId get() = if (hasTripId()) tripId else null + @JvmStatic @JvmOverloads fun GtfsRealtime.TripDescriptor.ModifiedTripSelector.toStringExt(short: Boolean = false) = buildString { diff --git a/src/main/java/org/mtransit/android/commons/provider/nextbus/NextBusStorage.kt b/src/main/java/org/mtransit/android/commons/provider/nextbus/NextBusStorage.kt new file mode 100644 index 00000000..7e8f0f8e --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/nextbus/NextBusStorage.kt @@ -0,0 +1,64 @@ +package org.mtransit.android.commons.provider.nextbus + +import android.content.Context +import androidx.annotation.WorkerThread +import org.mtransit.android.commons.PreferenceUtils + +object NextBusStorage { + + // region Vehicle location + + /** + * Override if multiple [org.mtransit.android.commons.provider.NextBusProvider] implementations in same app. + */ + private const val PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS = "pNextBusVehicleLocationsLastUpdate" + + @JvmStatic + @WorkerThread + fun getVehicleLocationLastUpdateMs(context: Context, default: Long) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS, default) + + @JvmStatic + @WorkerThread + fun saveVehicleLocationLastUpdateMs(context: Context, lastUpdateInMs: Long) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_MS, lastUpdateInMs) + } + + /** + * Override if multiple [org.mtransit.android.commons.provider.NextBusProvider] implementations in same app. + */ + private const val PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE = "pNextBusVehicleLocationLastUpdateCode" + + @JvmStatic + @WorkerThread + fun getVehicleLocationLastUpdateCode(context: Context, default: Int) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE, default) + + @JvmStatic + @WorkerThread + fun saveVehicleLocationLastUpdateCode(context: Context, code: Int) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_VEHICLE_LOCATION_LAST_UPDATE_CODE, code) + } + + // endregion + + // region Service update (messages) + + /** + * Override if multiple [org.mtransit.android.commons.provider.NextBusProvider] implementations in same app. + */ + private const val PREF_KEY_SERVICE_UPDATE_LAST_UPDATE_MS = "pNextBusMessagesLastUpdate" + + @JvmStatic + @WorkerThread + fun getServiceUpdateLastUpdateMs(context: Context, default: Long) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_SERVICE_UPDATE_LAST_UPDATE_MS, default) + + @JvmStatic + @WorkerThread + fun saveServiceUpdateLastUpdateMs(context: Context, lastUpdateInMs: Long) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_SERVICE_UPDATE_LAST_UPDATE_MS, lastUpdateInMs) + } + + // endregion +} diff --git a/src/main/java/org/mtransit/android/commons/provider/nextbus/api/NextBusApi.kt b/src/main/java/org/mtransit/android/commons/provider/nextbus/api/NextBusApi.kt new file mode 100644 index 00000000..73249690 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/nextbus/api/NextBusApi.kt @@ -0,0 +1,21 @@ +package org.mtransit.android.commons.provider.nextbus.api + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +// https://retro.umoiq.com/xmlFeedDocs/NextBusXMLFeed.pdf +interface NextBusApi { + + companion object { + const val BASE_HOST_URL = "https://retro.umoiq.com/service/" + } + + @GET("publicJSONFeed") + fun getVehicleLocations( + @Query("command") command: String = "vehicleLocations", + @Query("a") agencyTag: String, + @Query("r") routeTag: String? = null, + @Query("t") lastTimestamp: Long? = 0, // 0 == all (avoid error in JSON response) + ): Call +} diff --git a/src/main/java/org/mtransit/android/commons/provider/nextbus/api/VehicleLocationsResponse.kt b/src/main/java/org/mtransit/android/commons/provider/nextbus/api/VehicleLocationsResponse.kt new file mode 100644 index 00000000..0df8bfce --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/nextbus/api/VehicleLocationsResponse.kt @@ -0,0 +1,29 @@ +package org.mtransit.android.commons.provider.nextbus.api + +import com.google.gson.annotations.SerializedName + +data class VehicleLocationsResponse( + @SerializedName("vehicle") + val vehicle: List?, +) { + data class Vehicle( + @SerializedName("id") + val id: String?, + @SerializedName("routeTag") + val routeTag: String?, + @SerializedName("dirTag") + val dirTag: String?, + @SerializedName("lat") + val lat: Double?, + @SerializedName("lon") + val lon: Double?, + @SerializedName("secsSinceReport") + val secsSinceReport: Int?, + @SerializedName("predictable") + val predictable: Boolean?, + @SerializedName("heading") + val heading: Int?, + @SerializedName("speedKmHr") + val speedKmHr: Double?, + ) +} diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt new file mode 100644 index 00000000..e30f4d56 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt @@ -0,0 +1,302 @@ +package org.mtransit.android.commons.provider.vehiclelocations + +import android.content.Context +import com.google.transit.realtime.GtfsRealtime +import com.google.transit.realtime.GtfsRealtime.FeedMessage +import org.mtransit.android.commons.Constants +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.data.Direction +import org.mtransit.android.commons.data.Route +import org.mtransit.android.commons.data.RouteDirection +import org.mtransit.android.commons.data.RouteDirectionStop +import org.mtransit.android.commons.provider.GTFSRealTimeProvider +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteDirectionTagTargetUUID +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteTagTargetUUID +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID +import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optBearing +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLabel +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLatitude +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLongitude +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optPosition +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optSpeed +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestamp +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optVehicle +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.sortVehicles +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toVehicles +import org.mtransit.android.commons.provider.gtfs.makeRequest +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider.Companion.getCachedVehicleLocationsS +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation +import java.net.HttpURLConnection +import java.net.SocketException +import java.net.UnknownHostException +import kotlin.math.min +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +object GTFSRealTimeVehiclePositionsProvider { + + val VEHICLE_LOCATION_MAX_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds + + val VEHICLE_LOCATION_VALIDITY_IN_MS = 10.minutes.inWholeMilliseconds + val VEHICLE_LOCATION_VALIDITY_IN_FOCUS_IN_MS = 10.seconds.inWholeMilliseconds + + @Suppress("unused") + val VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_MS = 3.minutes.inWholeMilliseconds + + @Suppress("unused") + val VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = 1.minutes.inWholeMilliseconds + + @Suppress("unused") + @JvmStatic + fun GTFSRealTimeProvider.getMinDurationBetweenRefreshInMs(inFocus: Boolean) = + if (inFocus) VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS.adaptForCachedAPI(this.context) + else VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_MS.adaptForCachedAPI(this.context) + + @JvmStatic + fun GTFSRealTimeProvider.getValidityInMs(inFocus: Boolean) = + if (inFocus) VEHICLE_LOCATION_VALIDITY_IN_FOCUS_IN_MS.adaptForCachedAPI(this.context) + else VEHICLE_LOCATION_VALIDITY_IN_MS.adaptForCachedAPI(this.context) + + @JvmStatic + val GTFSRealTimeProvider.maxValidityInMs: Long get() = VEHICLE_LOCATION_MAX_VALIDITY_IN_MS.adaptForCachedAPI(this.context) + + private fun Long.adaptForCachedAPI(context: Context?) = + if (context?.let { GTFSRealTimeProvider.getAGENCY_VEHICLE_POSITIONS_URL_CACHED(it) }?.isNotBlank() == true) { + this * 2L // less calls to Cached API $$ + } else this + + @JvmStatic + fun GTFSRealTimeProvider.getCached(vehicleLocationFilter: VehicleLocationProviderContract.Filter) = + ((vehicleLocationFilter.poi as? RouteDirectionStop)?.getTargetUUIDs(this) + ?: vehicleLocationFilter.routeDirection?.getTargetUUIDs(this) + ?: vehicleLocationFilter.route?.getTargetUUIDs(this)) + ?.let { targetUUIDs -> + getCached(targetUUIDs, vehicleLocationFilter.tripIds) + } + + private fun RouteDirectionStop.getTargetUUIDs(provider: GTFSRealTimeProvider) = buildMap { + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), route.uuid) + getAgencyRouteDirectionTagTargetUUID(provider.agencyTag, getRouteTag(provider), getDirectionTag(provider))?.let { put(it, routeDirectionUUID) } + } + + private fun RouteDirection.getTargetUUIDs(provider: GTFSRealTimeProvider) = buildMap { + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), route.uuid) + getAgencyRouteDirectionTagTargetUUID(provider.agencyTag, getRouteTag(provider), getDirectionTag(provider))?.let { put(it, uuid) } + } + + private fun Route.getTargetUUIDs(provider: GTFSRealTimeProvider) = mapOf( + getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)) to uuid, + ) + + fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List? = null) = buildList { + getCachedVehicleLocationsS(targetUUIDs.keys, tripIds)?.let { + addAll(it) + } + }.map { it.copy(targetUUID = targetUUIDs[it.targetUUID] ?: it.targetUUID) } + + @JvmStatic + fun GTFSRealTimeProvider.getNew(vehicleLocationFilter: VehicleLocationProviderContract.Filter): List? { + updateAgencyDataIfRequired(vehicleLocationFilter.inFocusOrDefault) + return getCached(vehicleLocationFilter) + } + + private fun GTFSRealTimeProvider.updateAgencyDataIfRequired(inFocus: Boolean) { + val context = requireContextCompat() + var inFocus = inFocus + val lastUpdateInMs = GtfsRealTimeStorage.getVehicleLocationLastUpdateMs(context, 0L) + val lastUpdateCode = GtfsRealTimeStorage.getVehicleLocationLastUpdateCode(context, -1).takeIf { it >= 0 } + if (lastUpdateCode != null && lastUpdateCode != HttpURLConnection.HTTP_OK) { + inFocus = true // force earlier retry if last fetch returned HTTP error + } + val minUpdateMs = min(vehicleLocationMaxValidityInMs, getVehicleLocationValidityInMs(inFocus)) + val nowInMs = TimeUtils.currentTimeMillis() + if (lastUpdateInMs + minUpdateMs > nowInMs) { + return + } + updateAgencyDataIfRequiredSync(lastUpdateInMs, inFocus) + } + + @Synchronized + private fun GTFSRealTimeProvider.updateAgencyDataIfRequiredSync(lastUpdateInMs: Long, inFocus: Boolean) { + val context = requireContextCompat() + if (GtfsRealTimeStorage.getVehicleLocationLastUpdateMs(context, 0L) > lastUpdateInMs) { + return // too late, another thread already updated + } + val nowInMs = TimeUtils.currentTimeMillis() + var deleteAllRequired = false + if (lastUpdateInMs + vehicleLocationMaxValidityInMs < nowInMs) { + deleteAllRequired = true // too old to display + } + val minUpdateMs = min(vehicleLocationMaxValidityInMs, getVehicleLocationValidityInMs(inFocus)) + if (deleteAllRequired || lastUpdateInMs + minUpdateMs < nowInMs) { + updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update + } + } + + private fun GTFSRealTimeProvider.updateAllAgencyDataFromWWW(context: Context, deleteAllRequired: Boolean) { + var deleteAllDone = false + if (deleteAllRequired) { + deleteAllCachedVehicleLocations() + deleteAllDone = true + } + val newVehicleLocations = loadAgencyDataFromWWW(context) + if (newVehicleLocations != null) { // empty is OK + if (!deleteAllDone) { + deleteAllCachedVehicleLocations() + } + cacheVehicleLocations(newVehicleLocations) + } // else keep whatever we have until max validity reached + } + + private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(context: Context): List? { + try { + val urlRequest = makeRequest( + context, + urlCachedString = GTFSRealTimeProvider.getAGENCY_VEHICLE_POSITIONS_URL_CACHED(context), + getUrlString = { token -> GTFSRealTimeProvider.getAgencyVehiclePositionsUrlString(context, token) } + ) ?: return null + getOkHttpClient(context).newCall(urlRequest).execute().use { response -> + GtfsRealTimeStorage.saveVehicleLocationLastUpdateCode(context, response.code) + GtfsRealTimeStorage.saveVehicleLocationLastUpdateMs(context, TimeUtils.currentTimeMillis()) + when (response.code) { + HttpURLConnection.HTTP_OK -> { + val newLastUpdateInMs = TimeUtils.currentTimeMillis() + val vehicleLocations = mutableListOf() + val ignoreDirection = GTFSRealTimeProvider.isIGNORE_DIRECTION(context) + try { + val gFeedMessage = FeedMessage.parseFrom(response.body.bytes()) + val gVehiclePositions = gFeedMessage.entityList.toVehicles() + for (gVehiclePosition in gVehiclePositions.sortVehicles(newLastUpdateInMs)) { + if (Constants.DEBUG) { + MTLog.d( + this@GTFSRealTimeVehiclePositionsProvider, + "loadAgencyDataFromWWW() > GTFS vehicle: ${gVehiclePosition.toStringExt()}." + ) + } + processVehiclePositions(newLastUpdateInMs, gVehiclePosition, ignoreDirection) + ?.takeIf { it.isNotEmpty() } + ?.let { + vehicleLocations.addAll(it) + } + } + } catch (e: Exception) { + MTLog.w(this@GTFSRealTimeVehiclePositionsProvider, e, "loadAgencyDataFromWWW() > error while parsing GTFS Real Time data!") + } + MTLog.i(this@GTFSRealTimeVehiclePositionsProvider, "Found %d vehicle locations.", vehicleLocations.size) + if (Constants.DEBUG) { + for (vehicleLocation in vehicleLocations) { + MTLog.d(this@GTFSRealTimeVehiclePositionsProvider, "loadAgencyDataFromWWW() > - new ${vehicleLocation.toStringShort()}.") + } + } + return vehicleLocations + } + + else -> { + MTLog.w( + this@GTFSRealTimeVehiclePositionsProvider, + "ERROR: HTTP URL-Connection Response Code ${response.code} (Message: ${response.message})" + ) + return null + } + } + } + } catch (uhe: UnknownHostException) { + if (MTLog.isLoggable(android.util.Log.DEBUG)) { + MTLog.w(this@GTFSRealTimeVehiclePositionsProvider, uhe, "No Internet Connection!") + } else { + MTLog.w(this@GTFSRealTimeVehiclePositionsProvider, "No Internet Connection!") + } + return null + } catch (se: SocketException) { + MTLog.w(this@GTFSRealTimeVehiclePositionsProvider, se, "No Internet Connection!") + return null + } catch (e: Exception) { // Unknown error + MTLog.e(this@GTFSRealTimeVehiclePositionsProvider, e, "INTERNAL ERROR: Unknown Exception") + return null + } + } + + private fun GTFSRealTimeProvider.processVehiclePositions( + newLastUpdateInMs: Long, + gVehiclePosition: GtfsRealtime.VehiclePosition, + ignoreDirection: Boolean, + ): Set? { + val targetUUIDs = parseProviderTargetUUID(gVehiclePosition, ignoreDirection)?.takeIf { it.isNotBlank() } ?: return null + return setOf( + VehicleLocation( + authority = this.authority, + targetUUID = targetUUIDs, + targetTripId = gVehiclePosition.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), + lastUpdateInMs = newLastUpdateInMs, + maxValidityInMs = this@processVehiclePositions.vehicleLocationMaxValidityInMs, + // + vehicleId = gVehiclePosition.optVehicle?.optId, + vehicleLabel = gVehiclePosition.optVehicle?.optLabel, + reportTimestamp = gVehiclePosition.optTimestamp?.seconds, + latitude = gVehiclePosition.optPosition?.optLatitude ?: return null, + longitude = gVehiclePosition.optPosition?.optLongitude ?: return null, + bearingDegrees = gVehiclePosition.optPosition?.optBearing?.toInt(), // in degrees + speedMetersPerSecond = gVehiclePosition.optPosition?.optSpeed?.toInt(), // in meters per second + ) + ) + } + + private fun GTFSRealTimeProvider.parseProviderTargetUUID(gVehiclePosition: GtfsRealtime.VehiclePosition, ignoreDirection: Boolean): String? { + val tripDescriptor = gVehiclePosition.optTrip ?: return null + if (tripDescriptor.hasModifiedTrip() || tripDescriptor.hasStartTime() || tripDescriptor.hasStartDate()) { + MTLog.d(this, "parseTargetUUID() > unhandled values: ${tripDescriptor.toStringExt()}") + } + when (tripDescriptor.scheduleRelationship) { + GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED -> {} // handled + GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.UNSCHEDULED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.REPLACEMENT, + GtfsRealtime.TripDescriptor.ScheduleRelationship.DUPLICATED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.DELETED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, + -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${tripDescriptor.scheduleRelationship}") + } + return if (tripDescriptor.hasRouteId()) { + if (tripDescriptor.hasDirectionId() && !ignoreDirection) { + getAgencyRouteDirectionTagTargetUUID( + agencyTag, + tripDescriptor.routeId.originalIdToHash(routeIdCleanupPattern), + tripDescriptor.directionId, + ) + } else { + getAgencyRouteTagTargetUUID( + agencyTag, + tripDescriptor.routeId.originalIdToHash(routeIdCleanupPattern), + ) + } + } else { + getAgencyTagTargetUUID( + agencyTag + ) + } + } + + private val GTFSRealTimeProvider.routeIdCleanupPattern get() = getRouteIdCleanupPattern(requireContextCompat()) + private val GTFSRealTimeProvider.tripIdCleanupPattern get() = getTripIdCleanupPattern(requireContextCompat()) + + private val GTFSRealTimeProvider.agencyTag get() = getAgencyTag(requireContextCompat()) + + private fun Route.getRouteTag(provider: GTFSRealTimeProvider) = provider.getRouteTag(this) + private fun Direction.getDirectionTag(provider: GTFSRealTimeProvider) = provider.getDirectionTag(this) + + private fun RouteDirection.getRouteTag(provider: GTFSRealTimeProvider) = this.route.getRouteTag(provider) + private fun RouteDirection.getDirectionTag(provider: GTFSRealTimeProvider) = this.direction.getDirectionTag(provider) + + private fun RouteDirectionStop.getRouteTag(provider: GTFSRealTimeProvider) = this.route.getRouteTag(provider) + private fun RouteDirectionStop.getDirectionTag(provider: GTFSRealTimeProvider) = this.direction.getDirectionTag(provider) +} diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/NextBusVehicleLocationsProvider.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/NextBusVehicleLocationsProvider.kt new file mode 100644 index 00000000..10425c0b --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/NextBusVehicleLocationsProvider.kt @@ -0,0 +1,244 @@ +package org.mtransit.android.commons.provider.vehiclelocations + +import android.content.Context +import org.mtransit.android.commons.Constants +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.data.Route +import org.mtransit.android.commons.data.RouteDirection +import org.mtransit.android.commons.data.RouteDirectionStop +import org.mtransit.android.commons.provider.NextBusProvider +import org.mtransit.android.commons.provider.NextBusProvider.getAGENCY_TAG +import org.mtransit.android.commons.provider.NextBusProvider.getAgencyRouteTagTargetUUID +import org.mtransit.android.commons.provider.NextBusProvider.isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG +import org.mtransit.android.commons.provider.nextbus.NextBusStorage +import org.mtransit.android.commons.provider.nextbus.api.NextBusApi +import org.mtransit.android.commons.provider.nextbus.api.VehicleLocationsResponse +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider.Companion.getCachedVehicleLocationsS +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation +import java.net.HttpURLConnection +import java.net.SocketException +import java.net.UnknownHostException +import kotlin.math.min +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +object NextBusVehicleLocationsProvider { + + val VEHICLE_LOCATION_MAX_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds + + val VEHICLE_LOCATION_VALIDITY_IN_MS = 10.minutes.inWholeMilliseconds + val VEHICLE_LOCATION_VALIDITY_IN_FOCUS_IN_MS = 5.seconds.inWholeMilliseconds + + @Suppress("unused") + val VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_MS = 3.minutes.inWholeMilliseconds + + @Suppress("unused") + val VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = 1.minutes.inWholeMilliseconds + + @Suppress("unused") + @JvmStatic + fun getMinDurationBetweenRefreshInMs(inFocus: Boolean) = + if (inFocus) VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS + else VEHICLE_LOCATION_MIN_DURATION_BETWEEN_REFRESH_IN_MS + + @JvmStatic + fun getValidityInMs(inFocus: Boolean) = + if (inFocus) VEHICLE_LOCATION_VALIDITY_IN_FOCUS_IN_MS + else VEHICLE_LOCATION_VALIDITY_IN_MS + + @JvmStatic + val maxValidityInMs: Long get() = VEHICLE_LOCATION_MAX_VALIDITY_IN_MS + + @JvmStatic + fun NextBusProvider.getCached(vehicleLocationFilter: VehicleLocationProviderContract.Filter): List? = + ((vehicleLocationFilter.poi as? RouteDirectionStop)?.getTargetUUIDs(this) + ?: vehicleLocationFilter.routeDirection?.getTargetUUIDs(this) + ?: vehicleLocationFilter.route?.getTargetUUIDs(this)) + ?.let { targetUUIDs -> + getCached(targetUUIDs, tripIds = null) // NO GTFS trip.id information available + } + + private fun RouteDirectionStop.getTargetUUIDs(provider: NextBusProvider) = buildMap { + if (!provider.isAppendHeadSignValueToRouteTag) { + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), route.uuid) + } else { // STLaval + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), routeDirectionUUID) + } + } + + private fun RouteDirection.getTargetUUIDs(provider: NextBusProvider) = buildMap { + if (!provider.isAppendHeadSignValueToRouteTag) { + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), route.uuid) + } else { // STLaval + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), uuid) + } + } + + private fun Route.getTargetUUIDs(provider: NextBusProvider) = buildMap { + if (!provider.isAppendHeadSignValueToRouteTag) { + put(getAgencyRouteTagTargetUUID(provider.agencyTag, getRouteTag(provider)), uuid) + } // ELSE // STLaval + } + + fun NextBusProvider.getCached(targetUUIDs: Map, tripIds: List? = null) = buildList { + getCachedVehicleLocationsS(targetUUIDs.keys, tripIds)?.let { + addAll(it) + } + }.map { it.copy(targetUUID = targetUUIDs[it.targetUUID] ?: it.targetUUID) } + + @JvmStatic + fun NextBusProvider.getNew(vehicleLocationFilter: VehicleLocationProviderContract.Filter): List? { + updateAgencyDataIfRequired(vehicleLocationFilter.inFocusOrDefault) + return getCached(vehicleLocationFilter) + } + + private fun NextBusProvider.updateAgencyDataIfRequired(inFocus: Boolean) { + val context = requireContextCompat() + var inFocus = inFocus + val lastUpdateInMs = NextBusStorage.getVehicleLocationLastUpdateMs(context, 0L) + val lastUpdateCode = NextBusStorage.getVehicleLocationLastUpdateCode(context, -1).takeIf { it >= 0 } + if (lastUpdateCode != null && lastUpdateCode != HttpURLConnection.HTTP_OK) { + inFocus = true // force earlier retry if last fetch returned HTTP error + } + val minUpdateMs = min(vehicleLocationMaxValidityInMs, getVehicleLocationValidityInMs(inFocus)) + val nowInMs = TimeUtils.currentTimeMillis() + if (lastUpdateInMs + minUpdateMs > nowInMs) { + return + } + updateAgencyDataIfRequiredSync(lastUpdateInMs, inFocus) + } + + @Synchronized + private fun NextBusProvider.updateAgencyDataIfRequiredSync(lastUpdateInMs: Long, inFocus: Boolean) { + val context = requireContextCompat() + if (NextBusStorage.getVehicleLocationLastUpdateMs(context, 0L) > lastUpdateInMs) { + return // too late, another thread already updated + } + val nowInMs = TimeUtils.currentTimeMillis() + var deleteAllRequired = false + if (lastUpdateInMs + vehicleLocationMaxValidityInMs < nowInMs) { + deleteAllRequired = true // too old to display + } + val minUpdateMs = min(vehicleLocationMaxValidityInMs, getVehicleLocationValidityInMs(inFocus)) + if (deleteAllRequired || lastUpdateInMs + minUpdateMs < nowInMs) { + updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update + } + } + + private fun NextBusProvider.updateAllAgencyDataFromWWW(context: Context, deleteAllRequired: Boolean) { + var deleteAllDone = false + if (deleteAllRequired) { + deleteAllCachedVehicleLocations() + deleteAllDone = true + } + val newVehicleLocations = loadAgencyDataFromWWW(context) + if (newVehicleLocations != null) { // empty is OK + if (!deleteAllDone) { + deleteAllCachedVehicleLocations() + } + cacheVehicleLocations(newVehicleLocations) + } // else keep whatever we have until max validity reached + } + + private fun NextBusProvider.loadAgencyDataFromWWW(context: Context): List? { + try { + MTLog.i(this, "Loading from '%s' for agency '%s'...", NextBusApi.BASE_HOST_URL, agencyTag) + val response = getNextBusApi(context) + .getVehicleLocations(agencyTag = agencyTag) + .execute() + NextBusStorage.saveVehicleLocationLastUpdateCode(context, response.code()) + val newLastUpdate = TimeUtils.currentTimeMillis().milliseconds + NextBusStorage.saveVehicleLocationLastUpdateMs(context, newLastUpdate.inWholeMilliseconds) + when (response.code()) { + HttpURLConnection.HTTP_OK -> { + val vehicleLocations = mutableListOf() + try { + response.body()?.let { vehicleLocationsResponse -> + vehicleLocationsResponse.vehicle?.forEach { nVehicle -> + if (Constants.DEBUG) { + MTLog.d( + this@NextBusVehicleLocationsProvider, + "loadAgencyDataFromWWW() > NextBus nVehicle: ${nVehicle}." + ) + } + processVehiclePositions(newLastUpdate, nVehicle) + ?.takeIf { it.isNotEmpty() } + ?.let { + vehicleLocations.addAll(it) + } + } + } + } catch (e: Exception) { + MTLog.w(this@NextBusVehicleLocationsProvider, e, "loadAgencyDataFromWWW() > error while parsing NextBus Real Time data!") + } + MTLog.i(this@NextBusVehicleLocationsProvider, "Found %d vehicle locations.", vehicleLocations.size) + if (Constants.DEBUG) { + for (serviceUpdate in vehicleLocations) { + MTLog.d(this@NextBusVehicleLocationsProvider, "loadAgencyDataFromWWW() > vehicle location: $serviceUpdate.") + } + } + return vehicleLocations + } + + else -> { + MTLog.w( + this@NextBusVehicleLocationsProvider, + "ERROR: HTTP URL-Connection Response Code ${response.code()} (Message: ${response.message()})" + ) + return null + } + } + } catch (uhe: UnknownHostException) { + if (MTLog.isLoggable(android.util.Log.DEBUG)) { + MTLog.w(this@NextBusVehicleLocationsProvider, uhe, "No Internet Connection!") + } else { + MTLog.w(this@NextBusVehicleLocationsProvider, "No Internet Connection!") + } + return null + } catch (se: SocketException) { + MTLog.w(this@NextBusVehicleLocationsProvider, se, "No Internet Connection!") + return null + } catch (e: Exception) { // Unknown error + MTLog.e(this@NextBusVehicleLocationsProvider, e, "INTERNAL ERROR: Unknown Exception") + return null + } + } + + private fun NextBusProvider.processVehiclePositions( + newLastUpdate: Duration, + nVehicle: VehicleLocationsResponse.Vehicle, + ): Set? { + val targetUUIDs = parseProviderTargetUUID(nVehicle)?.takeIf { it.isNotBlank() } ?: return null + return setOf( + VehicleLocation( + authority = this.authority, + targetUUID = targetUUIDs, + targetTripId = null, // no GTFS trip.id info returned + lastUpdateInMs = newLastUpdate.inWholeMilliseconds, + maxValidityInMs = this@processVehiclePositions.vehicleLocationMaxValidityInMs, + // + vehicleId = nVehicle.id, + vehicleLabel = null, + reportTimestamp = nVehicle.secsSinceReport?.seconds?.let { newLastUpdate - it }, + latitude = nVehicle.lat?.toFloat() ?: return null, + longitude = nVehicle.lon?.toFloat() ?: return null, + bearingDegrees = nVehicle.heading, // in degrees + speedMetersPerSecond = nVehicle.speedKmHr?.div(3.6)?.toInt(), // in km/h (m/s = km/h * 1000 meters / 3600 seconds) + ) + ) + } + + private fun NextBusProvider.parseProviderTargetUUID(nVehicle: VehicleLocationsResponse.Vehicle) = + nVehicle.routeTag?.let { getAgencyRouteTagTargetUUID(agencyTag, it) } + + private val NextBusProvider.agencyTag get() = getAGENCY_TAG(requireContextCompat()) + private val NextBusProvider.isAppendHeadSignValueToRouteTag get() = isAPPEND_HEAD_SIGN_VALUE_TO_ROUTE_TAG(requireContextCompat()) + + private fun Route.getRouteTag(provider: NextBusProvider) = provider.getRouteTag(this, null) + private fun RouteDirection.getRouteTag(provider: NextBusProvider) = provider.getRouteTag(this) + private fun RouteDirectionStop.getRouteTag(provider: NextBusProvider) = provider.getRouteTag(this) +} diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationDbHelper.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationDbHelper.kt new file mode 100644 index 00000000..44ba542c --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationDbHelper.kt @@ -0,0 +1,56 @@ +package org.mtransit.android.commons.provider.vehiclelocations + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.provider.BaseColumns +import org.mtransit.android.commons.SqlUtils +import org.mtransit.android.commons.provider.common.MTSQLiteOpenHelper +import org.mtransit.commons.sql.SQLCreateBuilder.Companion.getNew + +abstract class VehicleLocationDbHelper( + context: Context?, + dbName: String, + factory: SQLiteDatabase.CursorFactory?, + dbVersion: Int, +) : MTSQLiteOpenHelper( + context, + dbName, + factory, + dbVersion, +) { + companion object { + const val T_VEHICLE_LOCATION = "vehicle_location" + + const val T_VEHICLE_LOCATION_K_ID: String = BaseColumns._ID + const val T_VEHICLE_LOCATION_K_TARGET_UUID = "target" + const val T_VEHICLE_LOCATION_K_TARGET_TRIP_ID = "target_trip_id" + const val T_VEHICLE_LOCATION_K_LAST_UPDATE = "last_update" + const val T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS = "max_validity" + + const val T_VEHICLE_LOCATION_K_VEHICLE_ID = "vehicle_id" + const val T_VEHICLE_LOCATION_K_VEHICLE_LABEL = "vehicle_label" + const val T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP = "report_timestamp" + const val T_VEHICLE_LOCATION_K_LATITUDE = "latitude" + const val T_VEHICLE_LOCATION_K_LONGITUDE = "longitude" + const val T_VEHICLE_LOCATION_K_BEARING = "bearing" + const val T_VEHICLE_LOCATION_K_SPEED = "speed" + + @JvmStatic + fun getSqlCreateBuilder(table: String) = getNew(table) + .appendColumn(T_VEHICLE_LOCATION_K_ID, SqlUtils.INT_PK_AUTO) + .appendColumn(T_VEHICLE_LOCATION_K_TARGET_UUID, SqlUtils.TXT) + .appendColumn(T_VEHICLE_LOCATION_K_TARGET_TRIP_ID, SqlUtils.TXT) + .appendColumn(T_VEHICLE_LOCATION_K_LAST_UPDATE, SqlUtils.INT) + .appendColumn(T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS, SqlUtils.INT) + // + .appendColumn(T_VEHICLE_LOCATION_K_VEHICLE_ID, SqlUtils.TXT) + .appendColumn(T_VEHICLE_LOCATION_K_VEHICLE_LABEL, SqlUtils.TXT) + .appendColumn(T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP, SqlUtils.INT) + .appendColumn(T_VEHICLE_LOCATION_K_LATITUDE, SqlUtils.REAL) + .appendColumn(T_VEHICLE_LOCATION_K_LONGITUDE, SqlUtils.REAL) + .appendColumn(T_VEHICLE_LOCATION_K_BEARING, SqlUtils.INT) + .appendColumn(T_VEHICLE_LOCATION_K_SPEED, SqlUtils.INT) + } + + abstract val dbName: String +} \ No newline at end of file diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProvider.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProvider.kt new file mode 100644 index 00000000..4aaaec8b --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProvider.kt @@ -0,0 +1,260 @@ +package org.mtransit.android.commons.provider.vehiclelocations + +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import androidx.core.database.sqlite.transaction +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.SqlUtils +import org.mtransit.android.commons.StringUtils +import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.provider.common.ContentProviderConstants +import org.mtransit.android.commons.provider.common.MTContentProvider +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation + +abstract class VehicleLocationProvider : MTContentProvider(), + VehicleLocationProviderContract { + + companion object { + private val LOG_TAG: String = VehicleLocationProvider::class.java.simpleName + + fun getNewUriMatcher(authority: String) = UriMatcher(UriMatcher.NO_MATCH).apply { + append(authority) + } + + @JvmStatic + fun UriMatcher.append(authority: String) { + addURI(authority, VehicleLocationProviderContract.PING_PATH, ContentProviderConstants.PING) + addURI(authority, VehicleLocationProviderContract.VEHICLE_LOCATION_PATH, ContentProviderConstants.VEHICLE_LOCATION) + } + + @JvmStatic + fun

P.queryS(uri: Uri, selection: String?): Cursor? { + return when (getURI_MATCHER().match(uri)) { + ContentProviderConstants.PING -> ContentProviderConstants.EMPTY_CURSOR // empty cursor = processed + ContentProviderConstants.VEHICLE_LOCATION -> getVehicleLocations(selection) + else -> null // not processed + } + } + + private fun

P.getVehicleLocations(selection: String?): Cursor { + val vehicleLocationFilter = VehicleLocationProviderContract.Filter.fromJSONString(selection) + if (vehicleLocationFilter == null) { + MTLog.w(LOG_TAG, "Error while parsing vehicle location filter! (%s)", selection) + return getVehicleLocationCursor(null) + } + val nowInMs = TimeUtils.currentTimeMillis() + val cachedVehicleLocations = getCachedVehicleLocations(vehicleLocationFilter)?.toMutableList() + var purgeNecessary = false + if (cachedVehicleLocations != null) { + val iterator = cachedVehicleLocations.iterator() + while (iterator.hasNext()) { + val cachedVehicleLocation = iterator.next() + if (cachedVehicleLocation.lastUpdateInMs + vehicleLocationMaxValidityInMs < nowInMs) { + iterator.remove() + purgeNecessary = true + } + } + } + if (purgeNecessary) { + purgeUselessCachedVehicleLocations() + } + if (cachedVehicleLocations != null) { + val it = cachedVehicleLocations.iterator() + while (it.hasNext()) { + val cachedVehicleLocation = it.next() + if (!cachedVehicleLocation.useful) { + cachedVehicleLocation.id?.let { + deleteCachedVehicleLocation(it) + } + it.remove() + } + } + } + if (vehicleLocationFilter.cacheOnlyOrDefault) { + if (cachedVehicleLocations.isNullOrEmpty()) { + MTLog.w(LOG_TAG, "getVehicleLocations() > No useful cache found!") + } + return getVehicleLocationCursor(cachedVehicleLocations) + } + val cacheValidityInMs = getVehicleLocationValidityInMs(vehicleLocationFilter.inFocusOrDefault) + // TODO filter cache validity override like service update? + var loadNewVehicleLocations = false + if (cachedVehicleLocations.isNullOrEmpty()) { + loadNewVehicleLocations = true + } else { + for (cachedVehicleLocation in cachedVehicleLocations) { + if (cachedVehicleLocation.lastUpdateInMs + cacheValidityInMs < nowInMs) { + loadNewVehicleLocations = true + break + } + } + } + if (loadNewVehicleLocations) { + val newVehicleLocations = getNewVehicleLocations(vehicleLocationFilter) + if (!newVehicleLocations.isNullOrEmpty()) { + return getVehicleLocationCursor(newVehicleLocations) + } + } + if (cachedVehicleLocations.isNullOrEmpty()) { + MTLog.w(LOG_TAG, "getVehicleLocations() > no cache & no data from provider for %s!", vehicleLocationFilter.uuid) + } + return getVehicleLocationCursor(cachedVehicleLocations) + } + + fun getVehicleLocationCursor(vehicleLocations: List?): Cursor { + if (vehicleLocations == null) { + return ContentProviderConstants.EMPTY_CURSOR + } + return MatrixCursor(VehicleLocationProviderContract.PROJECTION_VEHICLE_LOCATION) + .apply { + vehicleLocations.forEach { vehicleLocation -> + addRow(vehicleLocation.cursorRow) + } + } + } + + @JvmStatic + fun

P.getTypeS(uri: Uri): String? { + return when (getURI_MATCHER().match(uri)) { + ContentProviderConstants.PING, + ContentProviderConstants.VEHICLE_LOCATION -> StringUtils.EMPTY // empty string = processed + else -> null // not processed + } + } + + fun

P.getCachedVehicleLocationsS(targetUUIDs: Collection, tripIds: List? = null): List? { + return getCachedVehicleLocationsS( + this.contentUri, + buildString { + append(SqlUtils.getWhereInString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_UUID, targetUUIDs)) + tripIds?.takeIf { it.isNotEmpty() }?.let { + append(SqlUtils.AND) + append(SqlUtils.getWhereInString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID, it)) + } + } + ) + } + + @Suppress("unused") + fun

P.getCachedVehicleLocationsS(targetUUID: String): List? { + return getCachedVehicleLocationsS( + this.contentUri, + SqlUtils.getWhereEqualsString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_UUID, targetUUID) + ) + } + + //@formatter:off + @JvmStatic + private val VEHICLE_LOCATION_PROJECTION_MAP = SqlUtils.ProjectionMapBuilder.getNew() + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_ID, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_ID) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_TARGET_UUID, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_UUID) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_LAST_UPDATE, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LAST_UPDATE) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS) + + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_VEHICLE_ID, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_ID) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_VEHICLE_LABEL, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_LABEL) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_LATITUDE, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LATITUDE) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_LONGITUDE, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LONGITUDE) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_BEARING, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_BEARING) + .appendTableColumn(VehicleLocationDbHelper.T_VEHICLE_LOCATION, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_SPEED, VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_SPEED) + .build() + //@formatter:on + + private fun

P.getCachedVehicleLocationsS( + @Suppress("unused") uri: Uri?, + selection: String?, + ): List? = + try { + SQLiteQueryBuilder() + .apply { + tables = dbTableName + projectionMap = VEHICLE_LOCATION_PROJECTION_MAP + }.query( + getReadDB(), VehicleLocationProviderContract.PROJECTION_VEHICLE_LOCATION, selection, null, null, + null, null, null + ).use { cursor -> + buildList { + if (cursor != null && cursor.count > 0) { + if (cursor.moveToFirst()) { + do { + add(VehicleLocation.fromCursor(cursor, this@getCachedVehicleLocationsS.authority)) + } while (cursor.moveToNext()) + } + } + } + } + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "Error!") + null + } + + private val VehicleLocationProviderContract.contentUri: Uri + get() = Uri.withAppendedPath(this.authorityUri, VehicleLocationProviderContract.VEHICLE_LOCATION_PATH) + + @JvmStatic + @Synchronized + fun cacheVehicleLocationsS(provider: VehicleLocationProviderContract, newVehicleLocations: List?): Int { + var affectedRows = 0 + try { + provider.getWriteDB().transaction { + newVehicleLocations?.forEach { vehicleLocation -> + insert(provider.dbTableName, VehicleLocationDbHelper.T_VEHICLE_LOCATION_K_ID, vehicleLocation.toContentValues()) + .let { rowId -> + if (rowId > 0L) affectedRows++ + } + } + } + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "ERROR while applying batch update to the database!") + } + return affectedRows + } + + @JvmStatic + fun deleteAllCachedVehicleLocations(provider: VehicleLocationProviderContract): Boolean { + var deletedRows = 0 + try { + deletedRows = provider.getWriteDB().delete(provider.dbTableName, null, null) + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "Error while deleting ALL cached vehicle locations!") + } + return deletedRows > 0 + } + + @JvmStatic + fun deleteCachedVehicleLocation(provider: VehicleLocationProviderContract, vehicleLocationId: Int?): Boolean { + vehicleLocationId ?: return false + val selection = SqlUtils.getWhereEquals(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_ID, vehicleLocationId) + var deletedRows = 0 + try { + deletedRows = provider.getWriteDB().delete(provider.dbTableName, selection, null) + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "Error while deleting cached vehicle location '%s'!", vehicleLocationId) + } + return deletedRows > 0 + } + + @JvmStatic + fun purgeUselessCachedVehicleLocations(provider: VehicleLocationProviderContract): Boolean { + val oldestLastUpdate = TimeUtils.currentTimeMillis() - provider.vehicleLocationMaxValidityInMs + val selection = SqlUtils.getWhereInferior(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LAST_UPDATE, oldestLastUpdate) + var deletedRows = 0 + try { + deletedRows = provider.getWriteDB().delete(provider.dbTableName, selection, null) + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "Error while deleting cached vehicle locations!") + } + return deletedRows > 0 + } + } + + override fun getLogTag() = LOG_TAG +} + +private val VehicleLocationProviderContract.dbTableName: String + get() = this.vehicleLocationDbTableName diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProviderContract.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProviderContract.kt new file mode 100644 index 00000000..f930e0b0 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/VehicleLocationProviderContract.kt @@ -0,0 +1,228 @@ +package org.mtransit.android.commons.provider.vehiclelocations + +import android.annotation.SuppressLint +import android.net.Uri +import android.provider.BaseColumns +import androidx.annotation.Discouraged +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.MTLog.Loggable +import org.mtransit.android.commons.SecureStringUtils +import org.mtransit.android.commons.data.DefaultPOI +import org.mtransit.android.commons.data.POI +import org.mtransit.android.commons.data.Route +import org.mtransit.android.commons.data.RouteDirection +import org.mtransit.android.commons.provider.common.ProviderContract +import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation +import org.mtransit.commons.mapNotNullToMap + +interface VehicleLocationProviderContract : ProviderContract { + + companion object { + const val VEHICLE_LOCATION_PATH = "vehicle" + + const val PING_PATH = ProviderContract.PING_PATH + + /** + * see [VehicleLocation] + */ + val PROJECTION_VEHICLE_LOCATION = arrayOf( + Columns.T_VEHICLE_LOCATION_K_ID, + Columns.T_VEHICLE_LOCATION_K_TARGET_UUID, + Columns.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID, + Columns.T_VEHICLE_LOCATION_K_LAST_UPDATE, + Columns.T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS, + // + Columns.T_VEHICLE_LOCATION_K_VEHICLE_ID, + Columns.T_VEHICLE_LOCATION_K_VEHICLE_LABEL, + Columns.T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP, + Columns.T_VEHICLE_LOCATION_K_LATITUDE, + Columns.T_VEHICLE_LOCATION_K_LONGITUDE, + Columns.T_VEHICLE_LOCATION_K_BEARING, + Columns.T_VEHICLE_LOCATION_K_SPEED, + ) + } + + val authority: String + + val authorityUri: Uri + + val vehicleLocationMaxValidityInMs: Long + + fun getVehicleLocationValidityInMs(inFocus: Boolean): Long + + @Suppress("unused") + fun getMinDurationBetweenVehicleLocationRefreshInMs(inFocus: Boolean): Long + + fun cacheVehicleLocations(newVehicleLocations: List) + + fun getCachedVehicleLocations(vehicleLocationFilter: Filter): List? + + fun getNewVehicleLocations(vehicleLocationFilter: Filter): List? + + fun deleteCachedVehicleLocation(vehicleLocationId: Int): Boolean + fun purgeUselessCachedVehicleLocations(): Boolean + + val vehicleLocationDbTableName: String + + /** + * see [VehicleLocation] + */ + interface Columns { + companion object { + const val T_VEHICLE_LOCATION_K_ID: String = BaseColumns._ID + const val T_VEHICLE_LOCATION_K_TARGET_UUID = "target" + const val T_VEHICLE_LOCATION_K_TARGET_TRIP_ID = "target_trip_id" + const val T_VEHICLE_LOCATION_K_LAST_UPDATE = "last_update" + const val T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS = "max_validity" + + const val T_VEHICLE_LOCATION_K_VEHICLE_ID = "vehicle_id" + const val T_VEHICLE_LOCATION_K_VEHICLE_LABEL = "vehicle_label" + const val T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP = "report_timestamp" + const val T_VEHICLE_LOCATION_K_LATITUDE = "latitude" + const val T_VEHICLE_LOCATION_K_LONGITUDE = "longitude" + const val T_VEHICLE_LOCATION_K_BEARING = "bearing" + const val T_VEHICLE_LOCATION_K_SPEED = "speed" + } + } + + data class Filter @Discouraged("use from() instead") constructor( + val authority: String, + val poi: POI? = null, // RouteDirectionStop or DefaultPOI + val route: Route? = null, + val routeDirection: RouteDirection? = null, + val tripIds: List?, // original // GTFS // cleaned + ) : Loggable { + + var inFocus: Boolean? = null + val inFocusOrDefault get() = inFocus ?: false + + var cacheOnly: Boolean? = null + val cacheOnlyOrDefault get() = cacheOnly ?: false + + var providedEncryptKeysMap: Map? = null + private set + + @Discouraged("only for logs") + val targetUUIDs: List = buildList { + poi?.uuid?.let { add(it) } + route?.uuid?.let { add(it) } + routeDirection?.uuid?.let { add(it) } + } + + @SuppressLint("DiscouragedApi") + constructor(poi: POI, tripIds: List? = null) : + this(authority = poi.authority, poi = poi, tripIds = tripIds) + + @SuppressLint("DiscouragedApi") + constructor(route: Route, tripIds: List? = null) : + this(authority = route.authority, route = route, tripIds = tripIds) + + @SuppressLint("DiscouragedApi") + constructor(routeDirection: RouteDirection, tripIds: List? = null) : + this(authority = routeDirection.authority, routeDirection = routeDirection, tripIds = tripIds) + + @Suppress("unused") // main app only + fun appendProvidedKeys(keysMap: Map?): Filter { + keysMap?.mapNotNullToMap { (key, value) -> + SecureStringUtils.enc(value)?.let { encValue -> key to encValue } + }?.let { + providedEncryptKeysMap = it + } + return this + } + + fun getProvidedEncryptKey(key: String) = + this.providedEncryptKeysMap?.get(key)?.takeIf { it.isNotBlank() } + + companion object { + private val LOG_TAG: String = VehicleLocationProviderContract::class.java.simpleName + ">" + Filter::class.java.simpleName + + private const val JSON_AUTHORITY = "authority" + private const val JSON_POI = "poi" + private const val JSON_ROUTE = "route" + private const val JSON_ROUTE_DIRECTION = "routeDirection" + private const val JSON_TRIP_IDS = "tripIds" + private const val JSON_CACHE_ONLY = "cacheOnly" + private const val JSON_IN_FOCUS = "inFocus" + private const val JSON_PROVIDED_ENCRYPT_KEYS_MAP = "providedEncryptKeysMap" + + fun fromJSONString(jsonString: String?): Filter? { + try { + return if (jsonString == null) null else fromJSON(JSONObject(jsonString)) + } catch (jsone: JSONException) { + MTLog.w(LOG_TAG, jsone, "Error while parsing JSON string '$jsonString'") + return null + } + } + + @SuppressLint("DiscouragedApi") + fun fromJSON(json: JSONObject): Filter? { + val poi = json.optJSONObject(JSON_POI)?.let { jPoi -> + DefaultPOI.fromJSONStatic(jPoi) + } + val authority = JSONUtils.optString(json, JSON_AUTHORITY) + val route = json.optJSONObject(JSON_ROUTE)?.let { jRoute -> + authority?.let { Route.fromJSON(jRoute, it) } + } + val routeDirection = json.optJSONObject(JSON_ROUTE_DIRECTION)?.let { jRouteDirection -> + authority?.let { RouteDirection.fromJSON(jRouteDirection, it) } + } + val tripIds = json.optJSONArray(JSON_TRIP_IDS)?.let { jTripIds -> + buildList { + for (i in 0 until jTripIds.length()) { + add(jTripIds.getString(i)) + } + } + } + val inFocus = JSONUtils.optBoolean(json, JSON_IN_FOCUS) + val cacheOnly = JSONUtils.optBoolean(json, JSON_CACHE_ONLY) + val providedEncryptKeysMap: Map? = json.optJSONObject(JSON_PROVIDED_ENCRYPT_KEYS_MAP)?.let { jProvidedEncryptKeysMap -> + JSONUtils.toMapOfStrings(jProvidedEncryptKeysMap) + } + return (poi?.let { Filter(authority = it.authority, poi = it, tripIds = tripIds) } + ?: route?.let { Filter(authority = route.authority, route = it, tripIds = tripIds) } + ?: routeDirection?.let { Filter(authority = routeDirection.authority, routeDirection = it, tripIds = tripIds) }) + ?.apply { + this.inFocus = inFocus + this.cacheOnly = cacheOnly + this.providedEncryptKeysMap = providedEncryptKeysMap + } + } + + fun toJSONString(vehicleLocationFilter: Filter) = + toJSON(vehicleLocationFilter)?.toString() + + fun toJSON(vehicleLocationFilter: Filter): JSONObject? { + return try { + JSONObject().apply { + put(JSON_AUTHORITY, vehicleLocationFilter.authority) + vehicleLocationFilter.poi?.let { put(JSON_POI, it.toJSON()) } + vehicleLocationFilter.route?.let { put(JSON_ROUTE, Route.toJSON(it)) } + vehicleLocationFilter.routeDirection?.let { put(JSON_ROUTE_DIRECTION, RouteDirection.toJSON(it)) } + vehicleLocationFilter.tripIds?.let { put(JSON_TRIP_IDS, JSONArray(it)) } + vehicleLocationFilter.inFocus?.let { put(JSON_IN_FOCUS, it) } + vehicleLocationFilter.cacheOnly?.let { put(JSON_CACHE_ONLY, it) } + vehicleLocationFilter.providedEncryptKeysMap?.let { put(JSON_PROVIDED_ENCRYPT_KEYS_MAP, JSONUtils.toJSONObject(it)) } + } + } catch (jsone: JSONException) { + MTLog.w(LOG_TAG, jsone, "Error while making JSON object '$vehicleLocationFilter'!") + null + } + } + } + + override fun getLogTag() = LOG_TAG + + @Suppress("unused") // used from main app + fun toJSONString() = toJSONString(this) + + val uuid: String? + get() = poi?.uuid + ?: route?.uuid + ?: routeDirection?.uuid + } +} diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/model/VehicleLocation.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/model/VehicleLocation.kt new file mode 100644 index 00000000..1f63e3be --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/model/VehicleLocation.kt @@ -0,0 +1,115 @@ +package org.mtransit.android.commons.provider.vehiclelocations.model + +import android.content.ContentValues +import android.database.Cursor +import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.getFloat +import org.mtransit.android.commons.getLong +import org.mtransit.android.commons.getString +import org.mtransit.android.commons.optInt +import org.mtransit.android.commons.optLong +import org.mtransit.android.commons.optString +import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProviderContract +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * See [VehicleLocationProviderContract] + */ +data class VehicleLocation( + val id: Int? = null, // DB id + val authority: String, + val targetUUID: String, // route+direction or just route / routeTag / routeTag+dirTag + val targetTripId: String?, // cleaned + val lastUpdateInMs: Long, + val maxValidityInMs: Long, + // + val vehicleId: String?, // not user visible + val vehicleLabel: String?, // user visible + val reportTimestamp: Duration?, // in SECONDS + val latitude: Float, + val longitude: Float, + val bearingDegrees: Int?, // in degrees + val speedMetersPerSecond: Int?, // in m/s +) { + + val reportTimestampSec: Long? get() = reportTimestamp?.inWholeSeconds + + @Suppress("unused") + val reportTimestampMs: Long? get() = reportTimestamp?.inWholeMilliseconds + + val reportTimestampCountdown: Duration? get() = reportTimestamp?.let { (TimeUtils.currentTimeMillis().milliseconds - it) } + + private val _uid: String? = this.vehicleId ?: this.vehicleLabel + + val uuid: String? = _uid?.let { "${this.authority}-$it" } + + companion object { + @JvmStatic + fun fromCursor(cursor: Cursor, authority: String) = VehicleLocation( + id = cursor.optInt(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_ID), + authority = authority, + targetUUID = cursor.getString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_UUID), + targetTripId = cursor.optString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID), + lastUpdateInMs = cursor.getLong(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LAST_UPDATE), + maxValidityInMs = cursor.getLong(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS), + // + vehicleId = cursor.optString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_ID), + vehicleLabel = cursor.optString(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_LABEL), + reportTimestamp = cursor.optLong(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP)?.seconds, + latitude = cursor.getFloat(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LATITUDE), + longitude = cursor.getFloat(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LONGITUDE), + bearingDegrees = cursor.optInt(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_BEARING), + speedMetersPerSecond = cursor.optInt(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_SPEED), + ) + } + + fun toContentValues() = ContentValues().apply { + id?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_ID, it) } // ELSE AUTO INCREMENT + put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_UUID, targetUUID) + targetTripId?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_TARGET_TRIP_ID, it) } + put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LAST_UPDATE, lastUpdateInMs) + put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_MAX_VALIDITY_IN_MS, maxValidityInMs) + // + vehicleId?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_ID, it) } + vehicleLabel?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_LABEL, it) } + reportTimestampSec?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_VEHICLE_REPORT_TIMESTAMP, it) } + put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LATITUDE, latitude) + put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_LONGITUDE, longitude) + bearingDegrees?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_BEARING, it) } + speedMetersPerSecond?.let { put(VehicleLocationProviderContract.Columns.T_VEHICLE_LOCATION_K_SPEED, it) } + } + + /** + * see [VehicleLocationProviderContract.PROJECTION_VEHICLE_LOCATION] + */ + val cursorRow: Array get() = arrayOf( + id, + targetUUID, + targetTripId, + lastUpdateInMs, + maxValidityInMs, + // + vehicleId, + vehicleLabel, + reportTimestampSec, + latitude, + longitude, + bearingDegrees, + speedMetersPerSecond, + ) + + val useful: Boolean get() = this.lastUpdateInMs + this.maxValidityInMs >= TimeUtils.currentTimeMillis() + + @Suppress("unused") + fun toStringShort() = buildString { + append("VLoc:{") + vehicleId?.let { append("vId:").append(it).append(",") } + vehicleLabel?.let { append("vLabel:").append(it).append(",") } + targetTripId?.let { append("tTripId:").append(it).append(",") } + targetUUID.let { append("tUUID:").append(it).append(",") } + reportTimestampCountdown?.let { append("rCtSec:").append(it.inWholeSeconds).append(",") } + append("}") + } +} diff --git a/src/main/res/values/constants.xml b/src/main/res/values/constants.xml index b64dfab7..75137316 100644 --- a/src/main/res/values/constants.xml +++ b/src/main/res/values/constants.xml @@ -10,6 +10,8 @@ org.mtransit.android.provider.SCHEDULE_PROVIDER org.mtransit.android.providerSCHEDULE_PROVIDER_TARGET + org.mtransit.android.provider.VEHICLE_LOCATION_PROVIDER + org.mtransit.android.provider.VEHICLE_LOCATION_PROVIDER_TARGET org.mtransit.android.provider.SERVICE_UPDATE_PROVIDER org.mtransit.android.provider.SERVICE_UPDATE_PROVIDER_TARGET org.mtransit.android.provider.NEWS_PROVIDER diff --git a/src/main/res/values/gtfs_real_time_values.xml b/src/main/res/values/gtfs_real_time_values.xml index 9898db30..7ed6b17c 100755 --- a/src/main/res/values/gtfs_real_time_values.xml +++ b/src/main/res/values/gtfs_real_time_values.xml @@ -5,6 +5,8 @@ false + + false diff --git a/src/test/java/org/mtransit/android/commons/LocationUtilsTests.java b/src/test/java/org/mtransit/android/commons/LocationUtilsTests.java index e858ddd4..88cf5bc1 100644 --- a/src/test/java/org/mtransit/android/commons/LocationUtilsTests.java +++ b/src/test/java/org/mtransit/android/commons/LocationUtilsTests.java @@ -166,7 +166,7 @@ public POI getPOI() { return new RouteDirectionStop( 1, new Route(authority, rdsRouteTag, "R" + rdsRouteTag, "Route " + rdsRouteTag, "000000", rdsRouteTag.hashCode(), 0), - new Direction(rdsDirectionTag, Direction.HEADSIGN_TYPE_NONE, "head-sign " + rdsDirectionTag, rdsRouteTag), + new Direction(authority, rdsDirectionTag, Direction.HEADSIGN_TYPE_NONE, "head-sign " + rdsDirectionTag, rdsRouteTag), new Stop(rdsStopTag, String.valueOf(rdsStopTag), "Stop #" + rdsStopTag, 0.0d, 0.0d, 0, rdsStopTag.hashCode()), false ); diff --git a/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java b/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java index c32e4455..9c14b462 100644 --- a/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java +++ b/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java @@ -33,7 +33,7 @@ public class CaLTCOnlineProviderTest { private static final String AUTHORITY = "authority.test"; private static final Route DEFAULT_ROUTE = new Route(AUTHORITY, 1, "1", "route 1", "color", 1, 0); - private static final Direction DEFAULT_DIRECTION = new Direction(1, Direction.HEADSIGN_TYPE_STRING, "direction 1", 1); + private static final Direction DEFAULT_DIRECTION = new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "direction 1", 1); private static final Stop DEFAULT_STOP = new Stop(1, "1", "stop 1", 0, 0, 0, 1); private final CaLTCOnlineProvider provider = new CaLTCOnlineProvider(); diff --git a/src/test/java/org/mtransit/android/commons/provider/GrandRiverTransitProviderTests.java b/src/test/java/org/mtransit/android/commons/provider/GrandRiverTransitProviderTests.java index 6c726676..5b698d73 100644 --- a/src/test/java/org/mtransit/android/commons/provider/GrandRiverTransitProviderTests.java +++ b/src/test/java/org/mtransit/android/commons/provider/GrandRiverTransitProviderTests.java @@ -29,8 +29,10 @@ public class GrandRiverTransitProviderTests { private static final String VEHICLE_ID = "vehicle_id"; + private static final String AUTHORITY = "authority.test"; + private static final Route DEFAULT_ROUTE = new Route( - "authority.test", + AUTHORITY, 1, "1", "route 1", @@ -53,6 +55,7 @@ public void testParseAgencyJSONFirstAndLast() { // Arrange boolean noPickup = false; Direction direction = new Direction( + AUTHORITY, 1L, Direction.HEADSIGN_TYPE_STRING, "The Boardwalk", @@ -88,6 +91,7 @@ public void testParseAgencyJSONFirstAndLastNoPickup() { // Arrange boolean noPickup = true; Direction direction = new Direction( + AUTHORITY, 1L, Direction.HEADSIGN_TYPE_STRING, "Charles Term", // cleaned by parser @@ -123,6 +127,7 @@ public void testParseAgencyJSONSameTripDirectionWithDifferentHeadSign() { // Arrange boolean noPickup = false; Direction direction = new Direction( + AUTHORITY, 1L, Direction.HEADSIGN_TYPE_STRING, "The Boardwalk", @@ -158,6 +163,7 @@ public void testParseAgencyJSONSplitCircleWithEmptyHeadSign() { // Arrange boolean noPickup = false; Direction direction = new Direction( + AUTHORITY, 1L, Direction.HEADSIGN_TYPE_STRING, "Ainslie Term", // cleaned by parser diff --git a/src/test/java/org/mtransit/android/commons/provider/GreaterSudburyProviderTests.kt b/src/test/java/org/mtransit/android/commons/provider/GreaterSudburyProviderTests.kt index 8e500e52..ada3efea 100644 --- a/src/test/java/org/mtransit/android/commons/provider/GreaterSudburyProviderTests.kt +++ b/src/test/java/org/mtransit/android/commons/provider/GreaterSudburyProviderTests.kt @@ -21,7 +21,7 @@ class GreaterSudburyProviderTests { private const val AUTHORITY = "authority.test" private val DEFAULT_ROUTE = Route(AUTHORITY, 1, "1", "route 1", "color", 1, 0) - private val DEFAULT_Direction = Direction(1, Direction.HEADSIGN_TYPE_STRING, "Direction 1", 1) + private val DEFAULT_Direction = Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "Direction 1", 1) private val DEFAULT_STOP = Stop(1, "1", "stop 1", 0.0, 0.0, 0, 1) } diff --git a/src/test/java/org/mtransit/android/commons/provider/NextBusProviderTest.kt b/src/test/java/org/mtransit/android/commons/provider/NextBusProviderTest.kt index 7bbb3329..bf4c5906 100644 --- a/src/test/java/org/mtransit/android/commons/provider/NextBusProviderTest.kt +++ b/src/test/java/org/mtransit/android/commons/provider/NextBusProviderTest.kt @@ -12,9 +12,9 @@ class NextBusProviderTest { val stopTag = "1" val rdsTargetUUIDs = listOf( - NextBusProvider.getServiceUpdateAgencyTargetUUID(agencyTag), + NextBusProvider.getAgencyTargetUUID(agencyTag), NextBusProvider.getAgencyRouteStopTagTargetUUID(agencyTag, routeTag, stopTag), - NextBusProvider.getServiceUpdateAgencyRouteTagTargetUUID(agencyTag, routeTag), + NextBusProvider.getAgencyRouteTagTargetUUID(agencyTag, routeTag), ) Assert.assertEquals( diff --git a/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java b/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java index 1d5eb523..f8fa59b7 100644 --- a/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java +++ b/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java @@ -38,7 +38,7 @@ public class OCTranspoProviderTest { private static final String AUTHORITY = "authority.test"; private static final Route DEFAULT_ROUTE = new Route(AUTHORITY, 1, "1", "route 1", "color"); - private static final Direction DEFAULT_DIRECTION = new Direction(1, Direction.HEADSIGN_TYPE_STRING, "direction 1", 1); + private static final Direction DEFAULT_DIRECTION = new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "direction 1", 1); private static final Stop DEFAULT_STOP = new Stop(1, "1", "stop 1", 0, 0, 0, 1); private final Context context = mock(); @@ -89,7 +89,7 @@ public void testParseAgencyJSONArrivalsResults() { rds = new RouteDirectionStop( POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, DEFAULT_ROUTE, - new Direction(1, Direction.HEADSIGN_TYPE_STRING, "Greenboro", 1), + new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "Greenboro", 1), DEFAULT_STOP, false); long lastUpdateInMs = 1576984339000L; // December 21, 2019 10:12:19 PM GMT-05:00 @@ -128,7 +128,7 @@ public void testParseAgencyJSONArrivalsResults_OtherDirection() { rds = new RouteDirectionStop( POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, DEFAULT_ROUTE, - new Direction(1, Direction.HEADSIGN_TYPE_STRING, "Rockcliffe", 1), + new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "Rockcliffe", 1), DEFAULT_STOP, true); long lastUpdateInMs = 1576984339000L; // December 21, 2019 10:12:19 PM GMT-05:00 @@ -160,7 +160,7 @@ public void testParseAgencyJSONArrivalsResults_OneDirection() { rds = new RouteDirectionStop( POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, DEFAULT_ROUTE, - new Direction(1, Direction.HEADSIGN_TYPE_STRING, "Greenboro", 1), + new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "Greenboro", 1), DEFAULT_STOP, false); long lastUpdateInMs = 1576984339000L; // December 21, 2019 10:12:19 PM GMT-05:00 @@ -202,7 +202,7 @@ public void testParseAgencyJSONArrivalsResults_TwoDirections() { // stop: Lyon # rds = new RouteDirectionStop( POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, DEFAULT_ROUTE, - new Direction(1, Direction.HEADSIGN_TYPE_STRING, "Tunney's Pasture", 1), + new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "Tunney's Pasture", 1), DEFAULT_STOP, true); long lastUpdateInMs = 1576984320000L; // December 21, 2019 10:12:10 PM GMT-05:00 diff --git a/src/test/java/org/mtransit/android/commons/provider/OneBusAwayProviderTests.java b/src/test/java/org/mtransit/android/commons/provider/OneBusAwayProviderTests.java index 50dcff75..7301ec0d 100644 --- a/src/test/java/org/mtransit/android/commons/provider/OneBusAwayProviderTests.java +++ b/src/test/java/org/mtransit/android/commons/provider/OneBusAwayProviderTests.java @@ -165,6 +165,7 @@ public void testCleanDirectionHeadsignRDS() { String tripHeadsign = "Martin Grv Via Vaughan Metropolitan Ctr"; Direction direction = new Direction( + AUTHORITY, -1, Direction.HEADSIGN_TYPE_STRING, "Martin Grv", @@ -187,6 +188,7 @@ private RouteDirectionStop getRouteDirectionStop(String routeShortName, long rou "color" ); Direction direction = new Direction( + AUTHORITY, directionId, Direction.HEADSIGN_TYPE_STRING, "direction " + directionId, diff --git a/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java b/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java index 6144ca2b..43583cfd 100644 --- a/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java +++ b/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java @@ -45,7 +45,7 @@ public class StmInfoApiProviderTests { private static final String SOURCE_LABEL = "example.org"; private static final Route DEFAULT_ROUTE = new Route(AUTHORITY, 1, "1", "route 1", "color"); - private static final Direction DEFAULT_DIRECTION = new Direction(1, Direction.HEADSIGN_TYPE_STRING, "trip 1", 1); + private static final Direction DEFAULT_DIRECTION = new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, "trip 1", 1); private static final Stop DEFAULT_STOP = new Stop(1, "1", "stop 1", 0, 0, 0, 1); private final Context context = mock(); @@ -582,7 +582,7 @@ public void testParseAgencyJSONMessageResultsNoMessages() { rds = new RouteDirectionStop( POI.ITEM_VIEW_TYPE_ROUTE_DIRECTION_STOP, new Route(AUTHORITY, 1, routeShortName, "route 1", "color"), - new Direction(1, Direction.HEADSIGN_TYPE_STRING, headsignValue, 1), + new Direction(AUTHORITY, 1, Direction.HEADSIGN_TYPE_STRING, headsignValue, 1), DEFAULT_STOP, false); long newLastUpdateInMs = System.currentTimeMillis();