Page Menu
Home
GitPull.it
Search
Configure Global Search
Log In
Files
F13292336
D234.1778088332.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Authored By
Unknown
Size
64 KB
Referenced Files
None
Subscribers
None
D234.1778088332.diff
View Options
diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
--- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
+++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java
@@ -55,7 +55,7 @@
//.add(R.id.fragment_container_view, LinesDetailFragment.class,
// LinesDetailFragment.Companion.makeArgs("gtt:4U"))
- .add(R.id.fragment_container_view, MapLibreFragment.class, null)
+ .add(R.id.fragment_container_view, AlertsFragment.class, null)
.commit();
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
--- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java
@@ -40,6 +40,8 @@
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.internal.ViewModelProviders;
import androidx.preference.PreferenceManager;
import androidx.work.WorkInfo;
@@ -55,6 +57,7 @@
import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
import it.reyboz.bustorino.fragments.*;
import it.reyboz.bustorino.middleware.GeneralActivity;
+import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.backend.utils.openIceweasel;
@@ -71,6 +74,7 @@
private boolean showingMainFragmentFromOther = false;
private boolean onCreateComplete = false;
+ private ServiceAlertsViewModel serviceAlertsViewModel;
private final OnBackPressedCallback callback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
@@ -84,6 +88,8 @@
super.onCreate(savedInstanceState);
Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState);
setContentView(R.layout.activity_principal);
+ serviceAlertsViewModel = new ViewModelProvider(this).get(ServiceAlertsViewModel.class);
+
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
getWindow().setNavigationBarContrastEnforced(false);
}
@@ -101,7 +107,6 @@
else Log.w(DEBUG_TAG, "NO ACTION BAR");
mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this));
-
mDrawer = findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle(mToolbar);
@@ -300,13 +305,19 @@
-
//check if first run activity (IntroActivity) has been started once or not
final SharedPreferences theShPr = getMainSharedPreferences();
boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false);
if(!hasIntroRun){
startIntroductionActivity();
}
+ serviceAlertsViewModel.getLastTimeRunningDownload().observe(this, (timeRunning) -> {
+ if (timeRunning != null) {
+ Log.d(DEBUG_TAG, "requested alerts download at time: "+timeRunning);
+ }
+ });
+ serviceAlertsViewModel.launchPeriodDownload();
+
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
--- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java
@@ -51,7 +51,7 @@
}
- public static Notification makeMatoDownloadNotification(Context context,String title){
+ public static Notification makeDBUpdateLowPriorityNotification(Context context, String title){
return new NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID)
//.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE))
.setSmallIcon(R.drawable.ic_bus_stilized_transparent)
@@ -81,7 +81,7 @@
.build();
}
public static Notification makeMatoDownloadNotification(Context context){
- return makeMatoDownloadNotification(context, context.getString(R.string.downloading_data_mato));
+ return makeDBUpdateLowPriorityNotification(context, context.getString(R.string.downloading_data_mato));
}
public static Notification makeMQTTServiceNotification(Context context){
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtAlertsRequest.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtAlertsRequest.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtAlertsRequest.kt
@@ -0,0 +1,47 @@
+package it.reyboz.bustorino.backend.gtfs
+
+import com.android.volley.NetworkResponse
+import com.android.volley.Request
+import com.android.volley.Response
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.HttpHeaderParser
+import com.google.transit.realtime.GtfsRealtime
+import com.google.transit.realtime.GtfsRealtime.FeedEntity
+import it.reyboz.bustorino.backend.Fetcher
+import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest.RequestError
+
+class GtfsRtAlertsRequest(
+ errorListener: Response.ErrorListener,
+ val listener: Response.Listener<ArrayList<FeedEntity>>) :
+ Request<ArrayList<FeedEntity>>(Method.GET, GtfsUtils.GTFSRT_URL_ALERTS, errorListener) {
+ override fun parseNetworkResponse(response: NetworkResponse?): Response<ArrayList<FeedEntity>> {
+ if (response == null){
+ return Response.error(VolleyError("Response null"))
+ }
+ if (response.statusCode == 404){
+ return Response.error(VolleyError("404"))
+ }
+ else if (response.statusCode != 200){
+ return Response.error(VolleyError("200"))
+ }
+
+ val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data)
+
+ val alerts = ArrayList<FeedEntity>()
+ if(gtfsreq.hasHeader() && gtfsreq.entityCount>0){
+ for (i in 0 until gtfsreq.entityCount) {
+ val entity = gtfsreq.getEntity(i)
+
+ if (entity.hasAlert()){
+ alerts.add(entity)
+ }
+ }
+ }
+ return Response.success(alerts, HttpHeaderParser.parseCacheHeaders(response))
+ }
+
+ override fun deliverResponse(p0: ArrayList<FeedEntity>) {
+ listener.onResponse(p0)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt
--- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt
+++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt
@@ -29,7 +29,7 @@
class GtfsRtPositionsRequest(
errorListener: ErrorListener,
val listener: RequestListener) :
- Request<ArrayList<LivePositionUpdate>>(Method.GET, URL_POSITION, errorListener) {
+ Request<ArrayList<LivePositionUpdate>>(Method.GET, GtfsUtils.GTFSRT_URL_POSITION, errorListener) {
override fun parseNetworkResponse(response: NetworkResponse?): Response<ArrayList<LivePositionUpdate>> {
if (response == null){
@@ -67,10 +67,6 @@
}
companion object{
- const val URL_POSITION = "http://percorsieorari.gtt.to.it/das_gtfsrt/vehicle_position.aspx"
-
- const val URL_TRIP_UPDATES ="http://percorsieorari.gtt.to.it/das_gtfsrt/trip_update.aspx"
- const val URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx"
interface RequestListener{
fun onResponse(response: ArrayList<LivePositionUpdate>?)
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java
--- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java
@@ -23,6 +23,11 @@
abstract public class GtfsUtils {
+ public static final String GTFSRT_URL_POSITION = "http://percorsieorari.gtt.to.it/das_gtfsrt/vehicle_position.aspx";
+
+ public static final String GTFSRT_URL_TRIP_UPDATES ="http://percorsieorari.gtt.to.it/das_gtfsrt/trip_update.aspx";
+ public static final String GTFSRT_URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx";
+
public static String stripGtfsPrefix(String routeID){
String[] explo = routeID.split(":");
//default is
diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsAlertDBDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsAlertDBDownloadWorker.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsAlertDBDownloadWorker.kt
@@ -0,0 +1,119 @@
+package it.reyboz.bustorino.data
+
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import androidx.work.BackoffPolicy
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.ForegroundInfo
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkerParameters
+import com.android.volley.Response
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.RequestFuture
+import com.google.transit.realtime.GtfsRealtime
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.Notifications
+import it.reyboz.bustorino.backend.gtfs.GtfsRtAlertsRequest
+import it.reyboz.bustorino.data.GtfsMaintenanceWorker.Companion.OPERATION_TYPE
+import it.reyboz.bustorino.data.gtfs.GtfsAlertsActivePeriods
+import it.reyboz.bustorino.data.gtfs.GtfsAlertsTranslation
+import it.reyboz.bustorino.data.gtfs.GtfsAlertEntity
+import it.reyboz.bustorino.data.gtfs.GtfsAlertInformedEntity
+import it.reyboz.bustorino.data.gtfs.GtfsAlertsDBConverter
+import it.reyboz.bustorino.data.gtfs.GtfsDatabase
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class GtfsAlertDBDownloadWorker(appContext: Context, workerParams: WorkerParameters):
+ CoroutineWorker(appContext, workerParams) {
+ override suspend fun doWork(): Result {
+ val volleyManager = NetworkVolleyManager.getInstance(applicationContext)
+ val gtfsDatabase = GtfsDatabase.getGtfsDatabase(applicationContext)
+ //use future to wait for request
+
+ var attempts = 0
+ var notOK = true
+ var resuList = ArrayList<GtfsRealtime.FeedEntity>()
+ while (notOK && attempts < 5) {
+ Log.d(DEBUG_TAG, "Fetching alerts, trial $attempts")
+ val future = RequestFuture.newFuture<ArrayList<GtfsRealtime.FeedEntity>>()
+
+ val req = GtfsRtAlertsRequest(object : Response.ErrorListener {
+ override fun onErrorResponse(err: VolleyError) {
+ Log.e(DEBUG_TAG, "Error getting alerts: ${err.message}", err)
+ }
+ }, future)
+
+ volleyManager.requestQueue.add(req)
+ try {
+ resuList = future.get(10, TimeUnit.SECONDS)
+ if (resuList.isNotEmpty()){
+ Log.d(DEBUG_TAG, "Have no alerts, attempt $attempts")
+ notOK = false
+ }
+ } catch (e: InterruptedException) {
+ e.printStackTrace()
+ Log.e(DEBUG_TAG, e.message, e)
+ } catch (e: ExecutionException) {
+ e.printStackTrace()
+ Log.e(DEBUG_TAG, e.message, e)
+ } catch (e: TimeoutException) {
+ e.printStackTrace()
+ Log.e(DEBUG_TAG, e.message, e)
+ }
+
+ attempts++
+ }
+ if (notOK) {
+ return Result.failure()
+ }
+
+ val timeReceived = System.currentTimeMillis()
+ val alertsToAdd = ArrayList<GtfsAlertEntity>()
+ val translToAdd = ArrayList<GtfsAlertsTranslation>()
+ val activePeriods = ArrayList<GtfsAlertsActivePeriods>()
+ val informedEntities = ArrayList<GtfsAlertInformedEntity>()
+ for(e in resuList){
+ val parsedRes = GtfsAlertsDBConverter.fromFeedEntity(e, timeReceived)
+
+ alertsToAdd.add(parsedRes.alert)
+ translToAdd.addAll(parsedRes.translations)
+ activePeriods.addAll(parsedRes.activePeriods)
+ informedEntities.addAll(parsedRes.informedEntities)
+ }
+ Log.d(DEBUG_TAG, "alerts received: ${alertsToAdd.size}")
+ val dao =gtfsDatabase.alertsDao()
+ dao.insertMissingAlerts(alertsToAdd, translToAdd, activePeriods, informedEntities)
+
+ dao.deleteOlderThan48h(timeReceived)
+ return Result.success()
+ }
+
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+
+ val context = applicationContext
+ Notifications.createDBNotificationChannelIfNeeded(context)
+
+ return ForegroundInfo(NOTIFICATION_ID,
+ Notifications.makeDBUpdateLowPriorityNotification(context, context.getString(R.string.downloading_alerts_message)))
+ }
+
+
+ companion object{
+ private const val NOTIFICATION_ID = 271899102
+ private const val DEBUG_TAG = "BusTO-GTFSRTAlertsDown"
+
+ fun makeOneTimeRequest(tag: String): OneTimeWorkRequest {
+ //val data = Data.Builder().putString(OPERATION_TYPE, type).build()
+ return OneTimeWorkRequest.Builder(GtfsAlertDBDownloadWorker::class.java)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .addTag(tag)
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/GtfsRepository.kt
@@ -6,10 +6,16 @@
import it.reyboz.bustorino.data.gtfs.*
class GtfsRepository(
- val gtfsDao: GtfsDBDao
+ context: Context
) {
- constructor(context: Context) : this(GtfsDatabase.getGtfsDatabase(context).gtfsDao())
+ val gtfsDao: GtfsDBDao
+ val alertsDao: AlertsDao
+ init{
+ val gtfsDB = GtfsDatabase.getGtfsDatabase(context)
+ gtfsDao = gtfsDB.gtfsDao()
+ alertsDao = gtfsDB.alertsDao()
+ }
fun getLinesLiveDataForFeed(feed: String): LiveData<List<GtfsRoute>>{
//return withContext(Dispatchers.IO){
return gtfsDao.getRoutesForFeed(feed)
@@ -39,4 +45,8 @@
fun getRouteFromGtfsId(gtfsId: String): LiveData<GtfsRoute>{
return gtfsDao.getRouteByGtfsID(gtfsId)
}
+
+ fun getAlertsByRouteID(routeID: String): LiveData<List<AlertWithDetails>>{
+ return alertsDao.getAlertsForRoute(routeID)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/Converters.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/Converters.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/Converters.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/Converters.kt
@@ -18,6 +18,8 @@
package it.reyboz.bustorino.data.gtfs
import androidx.room.TypeConverter
+import com.google.transit.realtime.GtfsRealtime.Alert.Cause
+import com.google.transit.realtime.GtfsRealtime.Alert.Effect
import java.text.SimpleDateFormat
import java.util.*
@@ -29,7 +31,6 @@
*/
class Converters {
-
@TypeConverter
fun fromString(value: String?): Date? {
return dateFromFmtString(value)
@@ -48,6 +49,25 @@
fun fromInt(value: Int?): GtfsServiceDate.ExceptionType? {
return value?.let { GtfsServiceDate.ExceptionType.getByValue(it) }
}
+ // FOR GTFS REALTIME ENUMS
+ @TypeConverter
+ fun fromCause(cause: Cause): Int {
+ return cause.number
+ }
+
+ @TypeConverter
+ fun toCause(value: Int): Cause {
+ return Cause.forNumber(value) ?: Cause.UNKNOWN_CAUSE
+ }
+ @TypeConverter
+ fun fromEffect(effect: Effect): Int {
+ return effect.number
+ }
+
+ @TypeConverter
+ fun toEffect(value: Int): Effect {
+ return Effect.forNumber(value) ?: Effect.UNKNOWN_EFFECT
+ }
companion object{
const val DATE_FMT_STRING = "yyyyMMdd"
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDBConverter.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDBConverter.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDBConverter.kt
@@ -0,0 +1,135 @@
+package it.reyboz.bustorino.data.gtfs
+
+import com.google.transit.realtime.GtfsRealtime
+
+/**
+ * Risultato del mapping di un singolo FeedEntity:
+ * tutte le righe pronte per essere passate a [AlertDao.upsertAlert].
+ */
+data class MappedAlert(
+ val alert: GtfsAlertEntity,
+ val translations: List<GtfsAlertsTranslation>,
+ val activePeriods: List<GtfsAlertsActivePeriods>,
+ val informedEntities: List<GtfsAlertInformedEntity>
+)
+
+public object GtfsAlertsDBConverter {
+
+ /**
+ * Converte un FeedEntity GTFS-RT (che contiene un Alert) nelle entity Room.
+ *
+ * @param entity il FeedEntity dal feed. Deve avere `hasAlert() == true`.
+ * @param fetchedAtMillis epoch millis del momento di ricezione/salvataggio.
+ * @return null se il FeedEntity non contiene un alert (es. è un TripUpdate).
+ */
+ fun fromFeedEntity(
+ entity: GtfsRealtime.FeedEntity,
+ fetchedAtMillis: Long
+ ): MappedAlert {
+ if (!entity.hasAlert()) throw IllegalArgumentException("Alert entity can't be null")
+
+ val al = entity.alert
+ val alertId = entity.id
+
+ val alert = GtfsAlertEntity(
+ id = alertId,
+ cause = al.cause,
+ effect = al.effect,
+ fetchedAt = fetchedAtMillis,
+ userSeen = false
+ )
+
+ val translations = buildList {
+ // Header
+ if (al.hasHeaderText()) {
+ al.headerText.translationList.forEach { t ->
+ add(
+ GtfsAlertsTranslation(
+ alertId = alertId,
+ field = GtfsAlertsTranslation.FIELD_HEADER,
+ language = if (t.hasLanguage()) t.language else null,
+ text = t.text
+ )
+ )
+ }
+ }
+ // Description
+ if (al.hasDescriptionText()) {
+ al.descriptionText.translationList.forEach { t ->
+ add(
+ GtfsAlertsTranslation(
+ alertId = alertId,
+ field = GtfsAlertsTranslation.FIELD_DESCRIPTION,
+ language = if (t.hasLanguage()) t.language else null,
+ text = t.text
+ )
+ )
+ }
+ }
+ // URL (anche lui TranslatedString in GTFS-RT)
+ if (al.hasUrl()) {
+ al.url.translationList.forEach { t ->
+ add(
+ GtfsAlertsTranslation(
+ alertId = alertId,
+ field = GtfsAlertsTranslation.FIELD_URL,
+ language = if (t.hasLanguage()) t.language else null,
+ text = t.text
+ )
+ )
+ }
+ }
+ }
+
+ val activePeriods = al.activePeriodList.map { tr ->
+ GtfsAlertsActivePeriods(
+ alertId = alertId,
+ start = if (tr.hasStart()) tr.start else null,
+ end = if (tr.hasEnd()) tr.end else null
+ )
+ }
+
+ val informedEntities = al.informedEntityList.map { e ->
+
+
+ val (tripId, tripRouteId, directionId) = if (e.hasTrip()) {
+ val td = e.trip
+ Triple(
+ if (td.hasTripId()) "gtt:${td.tripId}" else null,
+ if (td.hasRouteId()) "gtt:${td.routeId}" else null,
+ if (td.hasDirectionId()) td.directionId else null
+ )
+ } else {
+ Triple(null, null, null)
+ }
+
+ GtfsAlertInformedEntity(
+ alertId = alertId,
+ //agencyId = if (e.hasAgencyId()) e.agencyId else null,
+ routeId = if (e.hasRouteId()) "gtt:${e.routeId}" else null,
+ routeType = if (e.hasRouteType()) e.routeType else null,
+ stopId = if (e.hasStopId()) e.stopId else null,
+ tripId = tripId,
+ tripRouteId = tripRouteId,
+ directionId = directionId
+ )
+ }
+
+ return MappedAlert(alert, translations, activePeriods, informedEntities)
+ }
+
+ /**
+ * Comodità: prende un intero FeedMessage e mappa solo i FeedEntity che sono alert,
+ * ignorando TripUpdate e VehiclePosition.
+ */
+ fun fromFeedMessage(
+ feed: GtfsRealtime.FeedMessage,
+ fetchedAtMillis: Long = System.currentTimeMillis()
+ ): List<MappedAlert> {
+ return feed.entityList.mapNotNull { fe ->
+ // Salta gli entity marcati come deleted
+ if (fe.isDeleted || !fe.hasAlert()) null
+ else fromFeedEntity(fe, fetchedAtMillis)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDao.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsAlertsDao.kt
@@ -0,0 +1,146 @@
+package it.reyboz.bustorino.data.gtfs
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+
+@Dao
+interface AlertsDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertAlert(alert: GtfsAlertEntity)
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertAlerts(alerts: List<GtfsAlertEntity>)
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertTranslations(items: List<GtfsAlertsTranslation>)
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertActivePeriods(items: List<GtfsAlertsActivePeriods>)
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertInformedEntities(items: List<GtfsAlertInformedEntity>)
+
+ @Query("DELETE FROM gtfsrt_alert_translations WHERE alertId = :id")
+ suspend fun deleteTranslationsFor(id: String)
+
+ @Query("DELETE FROM alerts_active_periods WHERE alertId = :id")
+ suspend fun deleteActivePeriodsFor(id: String)
+
+ @Query("DELETE FROM alerts_informed_entities WHERE alertId = :id")
+ suspend fun deleteInformedEntitiesFor(id: String)
+
+ /**
+ * Inserisce o aggiorna un alert e tutti i suoi figli atomicamente.
+ *
+ * Nota: se l'alert esiste già, ne preserviamo il valore di `seen` esistente
+ * (non vogliamo che un re-fetch del feed reimposti a false un alert già letto).
+ * Il chiamante può forzare un valore passandolo dentro `alert.seen`; in quel
+ * caso si usa quello.
+ */
+ @Transaction
+ suspend fun insertMissingAlerts(
+ alerts: List<GtfsAlertEntity>,
+ translations: List<GtfsAlertsTranslation>,
+ periods: List<GtfsAlertsActivePeriods>,
+ entities: List<GtfsAlertInformedEntity>,
+ preserveSeen: Boolean = true
+ ) {
+ /*
+ *** CONSIDER THIS if we ever need to replace the data instead of ignoring ***
+ val toInsert = if (preserveSeen) {
+ val existingSeen = isUserSeen(alert.id)
+ if (existingSeen != null) alert.copy(userSeen = existingSeen) else alert
+ } else {
+ alert
+ }
+ insertAlert(toInsert)
+
+ */
+
+
+ // Pulizia esplicita dei figli prima di reinserirli.
+ // Le CASCADE coprirebbero il caso di REPLACE su PK, ma essere espliciti
+ // evita sorprese e funziona anche se un giorno cambiamo strategia.
+ //deleteTranslationsFor(alert.id)
+ //deleteActivePeriodsFor(alert.id)
+ //deleteInformedEntitiesFor(alert.id)
+ if(alerts.isNotEmpty()) insertAlerts(alerts)
+ if (translations.isNotEmpty()) insertTranslations(translations)
+ if (periods.isNotEmpty()) insertActivePeriods(periods)
+ if (entities.isNotEmpty()) insertInformedEntities(entities)
+ }
+
+ // ---------- "Seen" flag ----------
+
+ @Query("SELECT userSeen FROM gtfsrt_alerts WHERE id = :id")
+ suspend fun isUserSeen(id: String): Boolean?
+
+ @Query("UPDATE gtfsrt_alerts SET userSeen = :seen WHERE id = :id")
+ suspend fun setSeen(id: String, seen: Boolean)
+
+ @Query("UPDATE gtfsrt_alerts SET userSeen = 1")
+ suspend fun markAllSeen()
+
+ //@Query("SELECT COUNT(*) FROM gtfsrt_alerts WHERE userSeen = 0")
+ //suspend fun countUnseen(): Int
+
+ // ---------- Read ----------
+
+ @Transaction
+ @Query("SELECT * FROM gtfsrt_alerts ORDER BY fetchedAt DESC")
+ fun getAllAlerts(): LiveData<List<AlertWithDetails>>
+
+ @Transaction
+ @Query("SELECT * FROM gtfsrt_alerts WHERE userSeen = 0 ORDER BY fetchedAt DESC")
+ suspend fun getUnseenAlerts(): List<AlertWithDetails>
+
+ @Transaction
+ @Query("SELECT * FROM gtfsrt_alerts WHERE id = :id")
+ suspend fun getAlert(id: String): AlertWithDetails?
+
+ @Transaction
+ @Query("""
+ SELECT a.* FROM gtfsrt_alerts a
+ INNER JOIN alerts_informed_entities ie ON ie.alertId = a.id
+ WHERE ie.stopId = :stopId
+ ORDER BY a.fetchedAt DESC
+ """)
+ fun getAlertsForStop(stopId: String): LiveData<List<AlertWithDetails>>
+
+ @Transaction
+ @Query("""
+ SELECT al.* FROM gtfsrt_alerts al
+ INNER JOIN alerts_informed_entities ie ON ie.alertId = al.id
+ WHERE ie.routeId = :routeId OR ie.tripRouteId = :routeId
+ ORDER BY al.fetchedAt DESC
+ """)
+ fun getAlertsForRoute(routeId: String): LiveData<List<AlertWithDetails>>
+
+ // ---------- Delete ----------
+
+ @Query("DELETE FROM gtfsrt_alerts WHERE id = :id")
+ suspend fun deleteAlert(id: String)
+
+ @Query("DELETE FROM gtfsrt_alerts")
+ suspend fun deleteAll()
+
+ /**
+ * Cancella tutti gli alert ricevuti più di 48 ore fa.
+ * Le CASCADE sulle FK puliscono automaticamente translations,
+ * active_periods e informed_entities.
+ *
+ * @param now epoch millis "adesso" (default: System.currentTimeMillis()).
+ * Esposto come parametro per facilitare i test.
+ * @return numero di righe cancellate.
+ */
+ @Query("DELETE FROM gtfsrt_alerts WHERE fetchedAt < :cutoff")
+ suspend fun deleteOlderThan(cutoff: Long): Int
+
+ suspend fun deleteOlderThan48h(now: Long = System.currentTimeMillis()): Int {
+ val cutoff = now - 48L * 60L * 60L * 1000L
+ return deleteOlderThan(cutoff)
+ }
+}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
@@ -34,11 +34,17 @@
GtfsTrip::class,
GtfsShape::class,
MatoPattern::class,
- PatternStop::class
+ PatternStop::class,
+ //entities for GTFS Realtime Alerts
+ GtfsAlertEntity::class,
+ GtfsAlertsTranslation::class,
+ GtfsAlertsActivePeriods::class,
+ GtfsAlertInformedEntity::class
],
version = GtfsDatabase.VERSION,
autoMigrations = [
- AutoMigration(from=2,to=3)
+ AutoMigration(from=2,to=3),
+ AutoMigration(from=3,to=4)
],
exportSchema = true
)
@@ -47,6 +53,8 @@
abstract fun gtfsDao() : GtfsDBDao
+ abstract fun alertsDao(): AlertsDao
+
companion object{
@Volatile
@@ -66,7 +74,7 @@
}
}
- const val VERSION = 3
+ const val VERSION = 4
const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE
val MIGRATION_1_2 = Migration(1,2) {
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRtAlerts.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRtAlerts.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRtAlerts.kt
@@ -0,0 +1,207 @@
+package it.reyboz.bustorino.data.gtfs
+
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import androidx.room.Relation
+import com.google.transit.realtime.GtfsRealtime.Alert.Cause
+import com.google.transit.realtime.GtfsRealtime.Alert.Effect
+import java.nio.Buffer
+import java.security.MessageDigest
+
+
+@Entity(tableName = "gtfsrt_alerts")
+data class GtfsAlertEntity(
+ /** FeedEntity.id dal feed GTFS-RT, unico nel FeedMessage. */
+ @PrimaryKey val id: String,
+
+ /** Alert.cause.name, es. "TECHNICAL_PROBLEM", "STRIKE", ... */
+ val cause: Cause,
+
+ /** Alert.effect.name, es. "NO_SERVICE", "DETOUR", ... */
+ val effect: Effect,
+
+ /** Timestamp (epoch millis) di quando questo alert è stato ricevuto/salvato. */
+ val fetchedAt: Long,
+
+ /** True se l'utente ha già visto/letto questo alert. Default false. */
+ val userSeen: Boolean = false
+)
+/**
+ * Traduzioni per i campi testuali dell'alert.
+ * `field` discrimina tra HEADER, DESCRIPTION e URL (tutti TranslatedString in GTFS-RT).
+ */
+@Entity(
+ tableName = "gtfsrt_alert_translations",
+ foreignKeys = [
+ ForeignKey(
+ entity = GtfsAlertEntity::class,
+ parentColumns = ["id"],
+ childColumns = ["alertId"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [Index("alertId")]
+)
+data class GtfsAlertsTranslation(
+ @PrimaryKey val hash: String,
+ val alertId: String,
+
+ /** "HEADER" | "DESCRIPTION" | "URL" */
+ val field: String,
+
+ /** BCP-47, può mancare nel feed (Translation.language è optional). */
+ val language: String?,
+
+ /** Translation.text è required nel .proto, quindi non-null qui. */
+ val text: String
+) {
+ constructor(alertId: String, field: String, language: String?, text: String) : this(
+ calcHash(alertId, field, language, text),
+ alertId,
+ field,
+ language,
+ text
+ )
+ companion object {
+ const val FIELD_HEADER = "HEADER"
+ const val FIELD_DESCRIPTION = "DESCRIPTION"
+ const val FIELD_URL = "URL"
+
+ fun calcHash(alertId: String, field: String, language: String?, text: String): String {
+ val md = MessageDigest.getInstance("MD5")
+ val coS = "$alertId|$field|$language|$text"
+ return md.digest(coS.toByteArray()).toHexString()
+ }
+ }
+
+}
+
+/**
+ * Un Alert può avere più TimeRange. Sia `start` che `end` sono optional nel.proto:
+ * - start mancante = "da sempre"
+ * - end mancante = "fino a tempo indeterminato"
+ */
+@Entity(
+ tableName = "alerts_active_periods",
+ foreignKeys = [
+ ForeignKey(
+ entity = GtfsAlertEntity::class,
+ parentColumns = ["id"],
+ childColumns = ["alertId"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [Index("alertId")],
+)
+data class GtfsAlertsActivePeriods(
+ @PrimaryKey val hash: String,
+ val alertId: String,
+
+ /** Epoch seconds (POSIX time, come da spec GTFS-RT). Null se non specificato. */
+ val start: Long?,
+ val end: Long?
+){
+ constructor(alertId: String, start: Long?, end: Long?) : this(
+ calcHash(alertId, start, end),
+ alertId, start, end
+ )
+ companion object{
+ fun calcHash(alertId: String, start: Long?, end: Long?): String {
+ val input = "${alertId}|${start ?: ""}|${end ?: ""}"
+ val md = MessageDigest.getInstance("MD5")
+ return md.digest(input.toByteArray()).toHexString()
+ }
+ }
+}
+/**
+ * Un EntitySelector dal feed. Tutti i campi sono optional nel .proto:
+ * almeno uno deve essere valorizzato, ma quale dipende dal feed.
+ */
+@Entity(
+ tableName = "alerts_informed_entities",
+ foreignKeys = [
+ ForeignKey(
+ entity = GtfsAlertEntity::class,
+ parentColumns = ["id"],
+ childColumns = ["alertId"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [
+ Index("alertId"),
+ Index("routeId"),
+ Index("stopId"),
+ Index("tripId")
+ ]
+)
+data class GtfsAlertInformedEntity(
+ @PrimaryKey val internalId: String,
+ val alertId: String,
+
+ val routeId: String?,
+ /** route_type GTFS (0=tram, 1=metro, 2=rail, 3=bus, ...). */
+ val routeType: Int?,
+ val stopId: String?,
+
+ /** Campi dal TripDescriptor annidato, se presente. */
+ val tripId: String?,
+ val tripRouteId: String?,
+ val directionId: Int?
+){
+ constructor(
+ alertId: String, routeId: String?, routeType: Int?, stopId: String?, tripId: String?, tripRouteId: String?, directionId: Int?
+ ): this(
+ calcHash(alertId, routeId, routeType, stopId, tripId, tripRouteId, directionId),
+ alertId,
+ routeId,
+ routeType,
+ stopId,
+ tripId,
+ tripRouteId,
+ directionId
+ )
+ companion object{
+ fun calcHash(alertId: String,routeId: String?, routeType: Int?, stopId: String?, tripId: String?, tripRouteId: String?, directionId: Int?): String {
+ val input = "${alertId}|${routeId ?: ""}|${routeType ?: ""}|${stopId ?: ""}|${tripId ?: ""}|${tripRouteId ?: ""}|${directionId ?: ""}"
+ val md = MessageDigest.getInstance("MD5")
+ return md.digest(input.toByteArray()).toHexString()
+ }
+ }
+}
+
+/**
+ * POJO di lettura: un alert con tutti i suoi figli.
+ * Usato dai @Query @Transaction nel DAO.
+ */
+data class AlertWithDetails(
+ @Embedded val alert: GtfsAlertEntity,
+
+ @Relation(parentColumn = "id", entityColumn = "alertId")
+ val translations: List<GtfsAlertsTranslation>,
+
+ @Relation(parentColumn = "id", entityColumn = "alertId")
+ val activePeriods: List<GtfsAlertsActivePeriods>,
+
+ @Relation(parentColumn = "id", entityColumn = "alertId")
+ val informedEntities: List<GtfsAlertInformedEntity>
+) {
+ fun longPrint(): String {
+ val sb = StringBuilder()
+ sb.append("======== ALERT ${alert.id} ======= \n")
+ for (t in translations){
+ sb.append(t.field).append("\n")
+ sb.append(t.language).append(" : ").append(t.text).append("\n")
+ }
+ val ies = informedEntities
+ sb.append("Valid for: ")
+ for (i in ies){
+ sb.append("Stop ${i.stopId}; Route ${i.routeId}; TripID ${i.tripId}; Trip Route ${i.tripRouteId}\n")
+ }
+ sb.append("\n")
+ return sb.toString()
+ }
+}
+
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
@@ -0,0 +1,67 @@
+package it.reyboz.bustorino.fragments
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.recyclerview.widget.RecyclerView
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel
+import kotlin.getValue
+
+
+class AlertsDialogFragment : DialogFragment() {
+ private var gtfsLineShow :String? = null
+
+ private lateinit var titleTextView: TextView
+ private lateinit var recyclerView: RecyclerView
+ private val alertsViewModel: ServiceAlertsViewModel by activityViewModels()
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let { args->
+ gtfsLineShow = args.getString(GTFS_LINE_ARG)
+ }
+
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Inflate the layout for this fragment
+ val root = inflater.inflate(R.layout.fragment_dialog_alerts_line, container, false)
+
+ titleTextView = root.findViewById<TextView>(R.id.titleTextView)
+ recyclerView = root.findViewById(R.id.alertsRecyclerView)
+ alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ alerts ->
+
+ }
+ return root
+ }
+
+ companion object {
+ /**
+ * Use this factory method to create a new instance of
+ * this fragment using the provided parameters.
+ *
+ * @param gtfsLine Line To show.
+ * @return A new instance of fragment LineAlertsDialogFragment.
+ */
+ @JvmStatic
+ fun newInstance(gtfsLine: String?) =
+ AlertsDialogFragment().apply {
+ arguments = Bundle().apply {
+ putString(GTFS_LINE_ARG, gtfsLine)
+ }
+ }
+ private const val GTFS_LINE_ARG = "gtfsLine"
+
+ private const val DEBUG_TAG = "BusTO-AlertsDialog"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsFragment.kt
@@ -0,0 +1,165 @@
+package it.reyboz.bustorino.fragments
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import com.google.transit.realtime.GtfsRealtime
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+
+/**
+ * A simple [Fragment] subclass.
+ * Use the [AlertsFragment.newInstance] factory method to
+ * create an instance of this fragment.
+ */
+class AlertsFragment : ScreenBaseFragment() {
+
+ private val alertsViewModel: ServiceAlertsViewModel by activityViewModels()
+
+ private lateinit var textView: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ //param1 = it.getString(ARG_PARAM1)
+ //param2 = it.getString(ARG_PARAM2)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Inflate the layout for this fragment
+ val root = inflater.inflate(R.layout.fragment_alerts, container, false)
+ textView = root.findViewById(R.id.simpleTextView)
+
+
+ alertsViewModel.allAlertsLiveData.observe(viewLifecycleOwner, { alerts ->
+ val sb = StringBuilder()
+ for (x in alerts) {
+ val al = x.alert
+ sb.append("======== ALERT ${al.id} ======= \n")
+ for (t in x.translations){
+ sb.append(t.field).append("\n")
+ sb.append(t.language).append(" : ").append(t.text).append("\n")
+ }
+ val ies = x.informedEntities
+ sb.append("Valid for: ")
+ for (i in ies){
+ sb.append("Stop ${i.stopId}; Route ${i.routeId}; TripID ${i.tripId}; Trip Route ${i.tripRouteId}\n")
+ }
+ sb.append("\n")
+ }
+
+ textView.text = sb.toString()
+ })
+
+
+ alertsViewModel.setStopFilter("472")
+ /*alertsViewModel.alertsForStop.observe(viewLifecycleOwner){
+ Log.d(DEBUG_TAG, "Got ${it.size} alerts")
+ it?.let {
+ showAlerts(it)
+ }
+ }
+
+ */
+ /*
+ alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner) { map ->
+ Log.d(DEBUG_TAG, "Alerts for routes: ${map.keys}")
+ val keys = map.keys
+ if(keys.isNotEmpty()){
+ val sb = StringBuilder()
+ for (key in keys.sorted()) {
+ sb.append(" ======== Route: $key =======").append("\n")
+ sb.append(makeAlertListText(map[key]!!)).append("\n")
+ Log.d(DEBUG_TAG, "Route: $key len: ${map[key]!!.size}")
+ }
+
+ textView.text = sb.toString()
+ }
+
+ }
+
+ */
+ return root
+ }
+
+ override fun getBaseViewForSnackBar(): View? {
+ TODO("Not yet implemented")
+ }
+
+ private fun makeAlertListText(alerts: List<GtfsRealtime.Alert>) : String{
+ val sb = StringBuilder()
+ for (al in alerts) {
+ sb.append("=========== Alert ===========\n")
+ sb.append("Title:\n")
+ for (t in al.headerText.translationList) {
+ sb.append(t.language).append(": ").append(t.text).append("\n")
+ }
+ sb.append("Description:\n")
+ val transl = al.descriptionText.translationList
+ for (t in transl) {
+ sb.append(t.language).append(": ").append(t.text).append("\n")
+ }
+ val infE = al.informedEntityList
+ sb.append("--- Active periods count: ${al.activePeriodCount}\n")
+ val timeActive = al.getActivePeriod(0)
+ sb.append("Start: ").append(getTimeStampToString(timeActive.start)).append(" ")
+ sb.append("End: ").append(getTimeStampToString(timeActive.end)).append("\n")
+ sb.append("--- Cause:\n")
+ sb.append(al.cause.name).append("\n")
+ sb.append("--- Informed entities:\n")
+ for (e in infE) {
+ if(e.hasTrip()){
+ sb.append("Trip: ${e.trip.tripId} for route ${e.trip.routeId}, ")
+ } else{
+ sb.append("No Trip, ")
+ }
+ sb.append("Stop: ${e.stopId}, Route: ${e.routeId}\n")
+ }
+ sb.append("\n")
+ }
+ return sb.toString()
+ }
+
+ companion object {
+ /**
+ * Use this factory method to create a new instance of
+ * this fragment using the provided parameters.
+ *
+ * @return A new instance of fragment AlertsFragment.
+ */
+ @JvmStatic
+ fun newInstance() =
+ AlertsFragment().apply {
+ arguments = Bundle().apply {
+ //putString(ARG_PARAM1, param1)
+ //putString(ARG_PARAM2, param2)
+ }
+ }
+
+ fun getTimeStampToString(timestamp: Long): String? {
+ val date = Date(timestamp*1000)
+
+ val sdf= SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+ sdf.timeZone = TimeZone.getTimeZone("Europe/Rome")
+
+ return sdf.format(date)
+ }
+
+ private const val DEBUG_TAG = "BusTO-AlertsFragment"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -34,6 +34,7 @@
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
@@ -55,6 +56,7 @@
import it.reyboz.bustorino.util.Permissions
import it.reyboz.bustorino.viewmodels.LinesViewModel
import it.reyboz.bustorino.viewmodels.MapStateViewModel
+import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel
import kotlinx.coroutines.Runnable
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
@@ -78,7 +80,7 @@
class LinesDetailFragment() : GeneralMapLibreFragment() {
- private var lineID = ""
+ private var lineID = "" // the GTFS line ID (e.g. "gtt:10U")
private lateinit var patternsSpinner: Spinner
private var patternsAdapter: ArrayAdapter<String>? = null
@@ -97,9 +99,11 @@
private var patternShown: MatoPatternWithStops? = null
private val viewModel: LinesViewModel by viewModels()
+ private val alertsViewModel: ServiceAlertsViewModel by activityViewModels()
//private var firstInit = true
private var pausedFragment = false
private lateinit var switchButton: ImageButton
+ private lateinit var lineInfoButton: ImageButton
private var favoritesButton: ImageButton? = null
private var locationIcon: ImageButton? = null
@@ -242,7 +246,7 @@
switchButton = rootView.findViewById(R.id.switchImageButton)
locationIcon = rootView.findViewById(R.id.locationEnableIcon)
busPositionsIconButton = rootView.findViewById(R.id.busPositionsImageButton)
-
+ lineInfoButton = rootView.findViewById(R.id.lineInfoWarningButton)
favoritesButton = rootView.findViewById(R.id.favoritesButton)
stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView)
descripTextView = rootView.findViewById(R.id.lineDescripTextView)
@@ -369,6 +373,20 @@
descripTextView.text = route.longName
descripTextView.visibility = View.VISIBLE
}
+ // enable info button if there are alerts on the line
+ alertsViewModel.setGtfsLineFilter(lineID)
+ alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ list ->
+ Log.d(DEBUG_TAG, "alerts for line $lineID: ${list.size}")
+
+ if(list.isNotEmpty()){
+ lineInfoButton.visibility = View.VISIBLE
+ Log.d(DEBUG_TAG, "First alert is:\n ${list[0].longPrint()}")
+ } else
+ lineInfoButton.visibility = View.GONE
+ }
+ lineInfoButton.setOnClickListener {
+ //TODO
+ }
/*
*/
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesGridShowingViewModel.kt
@@ -63,8 +63,7 @@
}
init {
- val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao()
- gtfsRepo = GtfsRepository(gtfsDao)
+ gtfsRepo = GtfsRepository(application)
routesLiveData = gtfsRepo.getAllRoutes()
filteredLinesLiveData.addSource(routesLiveData){
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt
@@ -38,8 +38,7 @@
stopsForPatternLiveData.postValue(stopsForPatternLiveData.value)
}
init {
- val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao()
- gtfsRepo = GtfsRepository(gtfsDao)
+ gtfsRepo = GtfsRepository(application)
oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application))
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
@@ -0,0 +1,179 @@
+package it.reyboz.bustorino.viewmodels
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.map
+import androidx.lifecycle.switchMap
+import androidx.lifecycle.viewModelScope
+import androidx.room.concurrent.AtomicBoolean
+import androidx.work.ExistingWorkPolicy
+import androidx.work.WorkManager
+import com.android.volley.Response
+import com.google.transit.realtime.GtfsRealtime
+import com.google.transit.realtime.GtfsRealtime.Alert
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.gtfs.GtfsRtAlertsRequest
+import it.reyboz.bustorino.data.GtfsAlertDBDownloadWorker
+import it.reyboz.bustorino.data.GtfsRepository
+import it.reyboz.bustorino.data.gtfs.AlertWithDetails
+import it.reyboz.bustorino.data.gtfs.GtfsDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+class ServiceAlertsViewModel(app: Application) : AndroidViewModel(app) {
+
+ private val gtfsRepo = GtfsRepository(app)
+ private val volleyManager = NetworkVolleyManager.getInstance(app)
+
+ private val alertsDao = GtfsDatabase.getGtfsDatabase(app).alertsDao()
+
+ private val workManager = WorkManager.getInstance(app)
+
+ //val alertsLiveData = MutableLiveData<ArrayList<Alert>>(ArrayList())
+
+ private val stopToFilter = MutableLiveData("")
+ private val routeToFilter = MutableLiveData("")
+
+ val lastTimeRunningDownload = MutableLiveData(0L)
+ private val keepRunning = AtomicBoolean(false)
+ fun setRunningDownloadRequests(value: Boolean) {
+ keepRunning.set(value)
+ }
+
+ val alertsByRouteLiveData = routeToFilter.switchMap {
+ gtfsRepo.getAlertsByRouteID(it)
+ }
+
+ val alertsByStopLiveData = stopToFilter.switchMap {
+ gtfsRepo.alertsDao.getAlertsForStop(it)
+ }
+
+ val allAlertsLiveData = gtfsRepo.alertsDao.getAllAlerts()
+ /*
+ private val volleyErrorListener = Response.ErrorListener { err ->
+ Log.e(DEBUG_TAG, "Error getting alerts: ${err.message}", err)
+ }
+ private var numTries = 0
+ private val responseListener = Response.Listener<ArrayList<GtfsRealtime.FeedEntity>> {
+ Log.d(DEBUG_TAG, "Received ${it.size} alerts")
+ if (it.isEmpty()) {
+ if(numTries<4){
+ numTries++;
+ requestAlerts()
+ Log.d(DEBUG_TAG, "Alerts requested again: $numTries")
+ }
+ }
+
+ alertsLiveData.postValue(it.map { it.alert })
+ }
+
+ private fun requestAlerts(){
+ val req = GtfsRtAlertsRequest(volleyErrorListener, responseListener)
+
+ volleyManager.requestQueue.add(req)
+ }
+
+ */
+
+ fun setStopFilter(stopId: String) {
+ stopToFilter.value = stopId
+ }
+ fun setGtfsLineFilter(routeId: String) {
+ routeToFilter.value = routeId
+ }
+
+ private fun downloadWorkIfTimePassed(){
+ val currentTime = System.currentTimeMillis()
+ if (lastTimeRunningDownload.value == 0L ||
+ currentTime > lastTimeRunningDownload.value!! + 5L*60*1000){
+ //actually enqueue request
+ Log.d(DEBUG_TAG, "Launching request to download alerts")
+ val req = GtfsAlertDBDownloadWorker.makeOneTimeRequest("alertsrn")
+ workManager.enqueueUniqueWork("AlertsDownloadsRun", ExistingWorkPolicy.KEEP, req)
+ lastTimeRunningDownload.value = System.currentTimeMillis()
+
+ viewModelScope.launch(Dispatchers.IO) {
+ delay((5L*60 +1).seconds)
+ if(keepRunning.get()) downloadWorkIfTimePassed()
+ }
+ }
+
+ }
+
+ fun launchPeriodDownload(){
+ setRunningDownloadRequests(true)
+ downloadWorkIfTimePassed()
+ }
+
+
+
+ private fun filterAlertsForStop(stopId: String, alerts: ArrayList<Alert>) : ArrayList<Alert>{
+
+ val filteredAlerts = ArrayList<Alert>()
+ for (al in alerts) {
+ for (ie in al.informedEntityList) {
+ if (ie.stopId == stopId) {
+ filteredAlerts.add(al)
+ }
+ }
+ }
+ return filteredAlerts
+ }
+
+ init{
+
+ /*
+ requestAlerts()
+
+ alertsByRouteLiveData.addSource(alertsLiveData){ alerts ->
+ if(alerts.isEmpty()){
+ return@addSource
+ }
+ val routeMap = HashMap<String, ArrayList<Alert>>()
+ for (al in alerts){
+ for( ie in al.informedEntityList){
+ var routeID = ""
+ if(ie.routeId.isNotEmpty()){
+ routeID = "gtt:${ie.routeId}"
+ } else if(ie.trip?.routeId?.isNotEmpty() == true){
+ routeID = "gtt:${ie.trip?.routeId}"
+ }
+ if (routeID.isNotEmpty()) {
+ if (!routeMap.containsKey(routeID)) {
+ routeMap[routeID] = ArrayList()
+ }
+
+ routeMap[routeID]!!.add(al)
+ }
+ }
+ }
+
+ alertsByRouteLiveData.postValue(routeMap)
+ }
+ // Set transformations for stop
+ alertsForStop.addSource(stopToFilter){ stopId ->
+ alertsLiveData.value?.let{
+ alertsForStop.postValue(filterAlertsForStop(stopId,it))
+ }
+ }
+
+ alertsForStop.addSource(alertsLiveData){ alerts ->
+ alertsForStop.postValue(filterAlertsForStop(stopToFilter.value!!,alerts))
+ }
+
+
+ */
+ }
+
+ companion object{
+ private const val DEBUG_TAG = "BusTO-GTFSRTAlerts"
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_bubble_warning_solid.xml b/app/src/main/res/drawable/chat_bubble_warning_solid.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/drawable/chat_bubble_warning_solid.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="#000000" android:fillType="evenOdd" android:pathData="M1.25,12C1.25,6.063 6.063,1.25 12,1.25C17.937,1.25 22.75,6.063 22.75,12C22.75,17.937 17.937,22.75 12,22.75C10.144,22.75 8.395,22.279 6.87,21.449L2.637,22.237C2.394,22.283 2.144,22.205 1.97,22.03C1.795,21.855 1.717,21.606 1.763,21.363L2.551,17.13C1.721,15.605 1.25,13.856 1.25,12ZM12,7.25C12.414,7.25 12.75,7.586 12.75,8V12C12.75,12.414 12.414,12.75 12,12.75C11.586,12.75 11.25,12.414 11.25,12V8C11.25,7.586 11.586,7.25 12,7.25ZM12.567,16.501C12.844,16.193 12.82,15.719 12.512,15.442C12.204,15.165 11.73,15.189 11.453,15.497L11.443,15.508C11.165,15.816 11.19,16.29 11.498,16.567C11.806,16.845 12.28,16.82 12.557,16.512L12.567,16.501Z" android:strokeWidth="1.5"/>
+
+</vector>
diff --git a/app/src/main/res/layout/fragment_alerts.xml b/app/src/main/res/layout/fragment_alerts.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/fragment_alerts.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".fragments.AlertsFragment">
+
+ <!-- TODO: Update blank fragment layout -->
+ <androidx.core.widget.NestedScrollView
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:layout_margin="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/hello_blank_fragment"
+ android:id="@+id/simpleTextView"
+ />
+ </androidx.core.widget.NestedScrollView>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dialog_alerts_line.xml b/app/src/main/res/layout/fragment_dialog_alerts_line.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/fragment_dialog_alerts_line.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="15dp"
+ tools:context=".fragments.AlertsDialogFragment">
+
+ <TextView
+ android:id="@+id/titleTextView"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:fontFamily="@font/lato_bold"
+ android:textColor="@color/black_900"
+ android:text="Alerts for line"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/alertsRecyclerView"
+ android:layout_below="@id/titleTextView"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="15dp"
+ app:fastScrollEnabled="true"
+ app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
+ app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
+
+ />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dialog_buspositions.xml b/app/src/main/res/layout/fragment_dialog_buspositions.xml
--- a/app/src/main/res/layout/fragment_dialog_buspositions.xml
+++ b/app/src/main/res/layout/fragment_dialog_buspositions.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml
--- a/app/src/main/res/layout/fragment_lines_detail.xml
+++ b/app/src/main/res/layout/fragment_lines_detail.xml
@@ -58,15 +58,17 @@
app:srcCompat="@drawable/ic_star_outline"
tools:ignore="OnClick"/>
</androidx.cardview.widget.CardView>
+
<TextView
android:text="DCCII"
android:layout_width="0dp"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
- android:layout_height="wrap_content" android:id="@+id/lineDescripTextView"
+ android:layout_height="wrap_content"
+ android:id="@+id/lineDescripTextView"
app:layout_constraintTop_toBottomOf="@id/switchImageButton"
- app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/lineInfoWarningButton"
+ app:layout_constraintStart_toStartOf="parent"
android:textColor="@color/grey_700"
android:fontFamily="@font/lato_regular"
android:textSize="18sp"
@@ -74,12 +76,40 @@
android:maxWidth="300sp"
android:layout_marginTop="12dp"
/>
+ <ImageButton
+ android:id="@+id/lineInfoWarningButton"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_gravity="end"
+ android:foreground="?attr/selectableItemBackground"
+ android:background="@android:color/transparent"
+ app:srcCompat="@drawable/chat_bubble_warning_solid"
+ app:tint="?colorPrimary"
+ android:layout_marginTop="0dp"
+ android:layout_marginEnd="5dp"
+ android:layout_marginStart="5dp"
+ android:layout_marginBottom="4dp"
+ app:layout_constraintTop_toTopOf="@id/lineDescripTextView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/lineDescripTextView"
+ app:layout_constraintBottom_toTopOf="@id/infoBarrier"
+ android:visibility="gone"
+ tools:ignore="OnClick"/>
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/infoBarrier"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="lineDescripTextView,lineInfoWarningButton"/>
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/patternsSpinner"
- app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/headingToTextView"
- android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@+id/lineDescripTextView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/headingToTextView"
+ android:layout_marginTop="6dp"
+ app:layout_constraintTop_toBottomOf="@+id/infoBarrier"
android:layout_marginStart="5dp"/>
<TextView
android:text="@string/direction_duep"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -230,7 +230,8 @@
<string name="database_notification_channel_desc">Updates of the app database</string>
<string name="live_position_service_name">BusTO - live position service</string>
<string name="live_positions_notification_channel">Live positions</string>
- <string name="live_positions_notification_channel_desc">Showing activity related to the live positions service</string>
+ <string name="live_positions_notification_channel_desc">Showing activity related to the live positions service
+ </string>
<string name="mqtt_notification_text">MaTO live bus positions service is running</string>
<string name="db_trips_download_message">Downloading trips from MaTO server</string>
@@ -258,6 +259,7 @@
<string name="search_box_lines_suggestion_filter">Filter by name</string>
<string name="requesting_db_update">Launching database update</string>
<string name="downloading_data_mato">Downloading data from MaTO server</string>
+ <string name="downloading_alerts_message">Downloading realtime alerts data</string>
<!-- preferences -->
<string name="pref_directions_capitalize">Capitalize directions</string>
@@ -374,7 +376,9 @@
<string name="positions_source_gtfsrt_short" translatable="false">GTFS RT</string>
<string name="live_positions_dialog_provider_text">Live positions source:</string>
<string name="live_positions_switch_provider">Switch source</string>
- <string name="live_positions_preference_switch_clear_title">Clear bus positions when switching live positions source</string>
+ <string name="live_positions_preference_switch_clear_title">Clear bus positions when switching live positions
+ source
+ </string>
<string name="updated_fill">Updated: %1$s</string>
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, May 6, 19:25 (19 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1891248
Default Alt Text
D234.1778088332.diff (64 KB)
Attached To
Mode
D234: Download GTFS RT Alerts and show them in the lines screen
Attached
Detach File
Event Timeline