Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/org/mtransit/android/commons/TimeUtilsK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ object TimeUtilsK {
if (negative) insert(0, "-")
}.trim()

@JvmStatic
fun currentInstant() = TimeUtils.currentTimeMillis().millisToInstant()

@JvmStatic
val EPOCH_TIME_0: Instant = 0L.millisToInstant()
}

Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/mtransit/android/commons/data/ServiceUpdate.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProviderContract;

import java.util.Comparator;
import java.util.Objects;

public class ServiceUpdate implements MTLog.Loggable {

Expand Down Expand Up @@ -228,6 +229,31 @@ public String toString() {
return sb.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServiceUpdate that = (ServiceUpdate) o;
return lastUpdateInMs == that.lastUpdateInMs &&
maxValidityInMs == that.maxValidityInMs &&
severity == that.severity &&
Objects.equals(id, that.id) &&
Objects.equals(targetUUID, that.targetUUID) &&
Objects.equals(targetTripId, that.targetTripId) &&
Objects.equals(text, that.text) &&
Objects.equals(textHTML, that.textHTML) &&
Objects.equals(noService, that.noService) &&
Objects.equals(language, that.language) &&
Objects.equals(sourceLabel, that.sourceLabel) &&
Objects.equals(sourceId, that.sourceId) &&
Objects.equals(originalId, that.originalId);
}

@Override
public int hashCode() {
return Objects.hash(id, targetUUID, targetTripId, lastUpdateInMs, maxValidityInMs, text, textHTML, severity, noService, language, sourceLabel, sourceId, originalId);
}

public boolean isUseful() {
return this.lastUpdateInMs + this.maxValidityInMs >= TimeUtils.currentTimeMillis();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ private static String getApiDirection(@NonNull Direction direction) {
return StringUtils.EMPTY;
}

private static final String SERVICE_UPDATE_SOURCE_ID = "api_stm_info_arrivals_messages";
public static final String SERVICE_UPDATE_SOURCE_ID = "api_stm_info_arrivals_messages";

private static final String APPLICATION_JSON = "application/JSON";
private static final String ACCEPT = "accept";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.mtransit.android.commons.provider.ca.info.stm

import android.content.Context
import android.util.Log
import androidx.annotation.VisibleForTesting
import org.mtransit.android.commons.Constants
import org.mtransit.android.commons.HtmlUtils
import org.mtransit.android.commons.LocaleUtils
Expand Down Expand Up @@ -32,6 +33,7 @@ import java.net.SocketException
import java.net.UnknownHostException
import java.util.Locale
import javax.net.ssl.SSLHandshakeException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
Expand All @@ -45,12 +47,17 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {

@JvmStatic
val SERVICE_UPDATE_MAX_VALIDITY_IN_MS = 1.days.inWholeMilliseconds
// .takeUnless { Constants.DEBUG } ?: 1.minutes.inWholeMilliseconds

val SERVICE_UPDATE_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds
// .takeUnless { Constants.DEBUG } ?: 1.minutes.inWholeMilliseconds
val SERVICE_UPDATE_VALIDITY_IN_FOCUS_IN_MS = 10.minutes.inWholeMilliseconds
// .takeUnless { Constants.DEBUG } ?: 1.minutes.inWholeMilliseconds

val SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_MS = 10.minutes.inWholeMilliseconds
// .takeUnless { Constants.DEBUG } ?: 1.minutes.inWholeMilliseconds
val SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = 1.minutes.inWholeMilliseconds
// .takeUnless { Constants.DEBUG } ?: 1.minutes.inWholeMilliseconds

@JvmStatic
fun getValidityInMs(inFocus: Boolean) =
Expand All @@ -60,8 +67,6 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {
fun getMinDurationBetweenRefreshInMs(inFocus: Boolean) =
if (inFocus) SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS else SERVICE_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_MS

private const val AGENCY_SOURCE_ID = "api_stm_info_messages"

@JvmStatic
fun StmInfoApiProvider.getCached(filter: ServiceUpdateProviderContract.Filter): List<ServiceUpdate>? {
return ((filter.poi as? RouteDirectionStop)?.getTargetUUIDs(includeStopTags = true)
Expand Down Expand Up @@ -129,7 +134,7 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {
if (!deleteAllDone) {
deleteAllAgencyServiceUpdateData()
}
cacheServiceUpdates(newServiceUpdates)
cacheServiceUpdates(newServiceUpdates.toList())
} // else keep whatever we have until max validity reached
}

Expand All @@ -154,7 +159,7 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {

private const val SERVICE_UPDATE_URL = "https://api.stm.info/pub/od/i3/v2/messages/etatservice"

private fun StmInfoApiProvider.loadAgencyDataFromWWW(context: Context): List<ServiceUpdate>? {
private fun StmInfoApiProvider.loadAgencyDataFromWWW(context: Context): Collection<ServiceUpdate>? {
try {
val call = getSERVICE_UPDATES_URL_CACHED(context).takeIf { it.isNotBlank() }?.let { urlCachedString ->
getStmInfoApi(context).getV2MessageEtatService(urlCachedString)
Expand All @@ -176,88 +181,15 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {
StmInfoServiceUpdateStorage.saveServiceUpdateLastUpdate(context, now)
when (response.code()) {
HttpURLConnection.HTTP_OK -> {
val serviceUpdates = mutableListOf<ServiceUpdate>()
val sourceLabel = SourceUtils.getSourceLabel( // always use source from official API
SERVICE_UPDATE_URL
)
val etatServiceResponse = response.body()
val headerTimestamp = etatServiceResponse?.header?.timestamp ?: now
etatServiceResponse?.alerts?.forEach { alert ->
if (!alert.isActive()) {
MTLog.d(this@StmInfoServiceUpdateProvider, "Ignore inactive alert. ($alert)")
return@forEach
}
val informedEntities = alert.informedEntities?.takeIf { it.isNotEmpty() }
?: run {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o informed entities! ($alert)")
return@forEach
}
val routeShortNames = informedEntities.mapNotNull { it.routeShortName }.takeIf { it.isNotEmpty() }
?: run {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o route short names! ($alert)")
return@forEach
}
val directionId = informedEntities.singleOrNull { !it.directionId.isNullOrBlank() }?.directionId
val stopIds = informedEntities.mapNotNull { it.stopCode }.toSet()

val targetUUIDs: Set<String> = buildSet {
routeShortNames.forEach { routeShortName ->
if (stopIds.isEmpty()) {
(getAgencyRouteDirectionTagTargetUUID(routeShortName, directionId)
?: getAgencyRouteTagTargetUUID(routeShortName)).let {
add(it)
}
} else {
stopIds.forEach { stopId ->
(getAgencyRouteDirectionStopTagTargetUUID(routeShortName, directionId, stopId)
?: getAgencyRouteStopTagTargetUUID(routeShortName, stopId)).let {
add(it)
}
}
}
}
}
val headerTexts = alert.headerTexts?.parseTranslations()
val descriptionTexts = alert.descriptionTexts?.parseTranslations()
val languages = headerTexts?.keys.orEmpty() + descriptionTexts?.keys.orEmpty()
if (languages.isEmpty()) {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o translations! ($alert)")
return@forEach
}
targetUUIDs.forEach { targetUUID ->
val severity = if (stopIds.isNotEmpty()) {
ServiceUpdate.SEVERITY_WARNING_POI
} else {
ServiceUpdate.SEVERITY_INFO_RELATED_POI
} // else ServiceUpdate.SEVERITY_INFO_UNKNOWN?
languages.forEach { language ->
val header = headerTexts?.get(language)
val description = descriptionTexts?.get(language)
?: return@forEach // no description == no service update to show
val replacement = ServiceUpdateCleaner.getReplacement(severity)
val descriptionHtml = description.let {
var textHtml = it
textHtml = HtmlUtils.toHTML(textHtml)
textHtml = HtmlUtils.fixTextViewBR(textHtml)
textHtml = ServiceUpdateCleaner.clean(textHtml, replacement, language)
textHtml
}
serviceUpdates.add(
makeServiceUpdate(
targetUUID = targetUUID,
lastUpdate = headerTimestamp,
maxValidity = serviceUpdateMaxValidity,
text = ServiceUpdateCleaner.makeText(header, description),
optTextHTML = ServiceUpdateCleaner.makeTextHTML(header, descriptionHtml),
severity = severity,
sourceId = AGENCY_SOURCE_ID,
sourceLabel = sourceLabel,
language = language
)
)
}
}
}
val serviceUpdates = etatServiceResponse.toServiceUpdates(
maxValidity = serviceUpdateMaxValidity,
sourceLabel = sourceLabel,
now = now,
)
MTLog.i(this@StmInfoServiceUpdateProvider, "Found %d service updates.", serviceUpdates.size)
if (Constants.DEBUG) {
for (serviceUpdate in serviceUpdates) {
Expand Down Expand Up @@ -299,6 +231,94 @@ object StmInfoServiceUpdateProvider : MTLog.Loggable {
}
}

@VisibleForTesting
internal fun EtatServiceResponse?.toServiceUpdates(
maxValidity: Duration,
sourceLabel: String,
now: Instant,
): Collection<ServiceUpdate> {
val serviceUpdates = mutableSetOf<ServiceUpdate>()
val alerts = this?.alerts?.takeIf { it.isNotEmpty() } ?: return serviceUpdates
val headerTimestamp = this.header?.timestamp ?: now
alerts.forEach { alert ->
if (!alert.isActive()) {
MTLog.d(this@StmInfoServiceUpdateProvider, "Ignore inactive alert. ($alert)")
return@forEach
}
val informedEntities = alert.informedEntities?.takeIf { it.isNotEmpty() }
?: run {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o informed entities! ($alert)")
return@forEach
}
val routeShortNames = informedEntities.mapNotNull { it.routeShortName }.takeIf { it.isNotEmpty() }
?: run {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o route short names! ($alert)")
return@forEach
}
val directionId = informedEntities.singleOrNull { !it.directionId.isNullOrBlank() }?.directionId
val stopIds = informedEntities.mapNotNull { it.stopCode }.toSet()

val targetUUIDs: Set<String> = buildSet {
routeShortNames.forEach { routeShortName ->
if (stopIds.isEmpty()) {
(getAgencyRouteDirectionTagTargetUUID(routeShortName, directionId)
?: getAgencyRouteTagTargetUUID(routeShortName)).let {
add(it)
}
} else {
stopIds.forEach { stopId ->
(getAgencyRouteDirectionStopTagTargetUUID(routeShortName, directionId, stopId)
?: getAgencyRouteStopTagTargetUUID(routeShortName, stopId)).let {
add(it)
}
}
}
}
}
val headerTexts = alert.headerTexts?.parseTranslations()
val descriptionTexts = alert.descriptionTexts?.parseTranslations()
val languages = headerTexts?.keys.orEmpty() + descriptionTexts?.keys.orEmpty()
if (languages.isEmpty()) {
MTLog.w(this@StmInfoServiceUpdateProvider, "Ignore alert w/o translations! ($alert)")
return@forEach
}
targetUUIDs.forEach { targetUUID ->
val severity = if (stopIds.isNotEmpty()) {
ServiceUpdate.SEVERITY_WARNING_POI
} else {
ServiceUpdate.SEVERITY_INFO_RELATED_POI
} // else ServiceUpdate.SEVERITY_INFO_UNKNOWN?
languages.forEach { language ->
val header = headerTexts?.get(language)
val description = descriptionTexts?.get(language)
?: return@forEach // no description == no service update to show
val replacement = ServiceUpdateCleaner.getReplacement(severity)
val descriptionHtml = description.let {
var textHtml = it
textHtml = HtmlUtils.toHTML(textHtml)
textHtml = HtmlUtils.fixTextViewBR(textHtml)
textHtml = ServiceUpdateCleaner.clean(textHtml, replacement, language)
textHtml
}
serviceUpdates.add(
makeServiceUpdate(
targetUUID = targetUUID,
lastUpdate = headerTimestamp,
maxValidity = maxValidity,
text = ServiceUpdateCleaner.makeText(header, description),
optTextHTML = ServiceUpdateCleaner.makeTextHTML(header, descriptionHtml),
severity = severity,
sourceId = StmInfoApiProvider.SERVICE_UPDATE_SOURCE_ID,
sourceLabel = sourceLabel,
language = language
)
)
}
}
}
return serviceUpdates
}

private fun List<EtatServiceResponse.Alert.TranslatedText>.parseTranslations(): Map<String, String>? {
this.takeIf { it.isNotEmpty() } ?: return null
var hasDefaultLanguage = false
Expand Down
Loading