Page MenuHomeGitPull.it

D234.1778075526.diff
No OneTemporary

Authored By
Unknown
Size
80 KB
Referenced Files
None
Subscribers
None

D234.1778075526.diff

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,7 @@
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import androidx.work.WorkInfo;
@@ -55,6 +56,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 +73,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 +87,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 +106,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 +304,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.launchAlertsPeriodCheck();
+
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
@@ -846,4 +856,16 @@
}
}
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // stop updating the alerts
+ serviceAlertsViewModel.setRunningDownloadRequests(false);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ serviceAlertsViewModel.launchAlertsPeriodCheck();
+ }
}
diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/AlertLineFullAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/AlertLineFullAdapter.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/adapters/AlertLineFullAdapter.kt
@@ -0,0 +1,56 @@
+package it.reyboz.bustorino.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.data.gtfs.AlertWithDetails
+import it.reyboz.bustorino.data.gtfs.GtfsAlertsTranslation
+
+class AlertLineFullAdapter(val alerts: List<AlertWithDetails>,
+ val locale: String
+ ) :RecyclerView.Adapter<AlertLineFullAdapter.ViewHolder>() {
+
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ViewHolder {
+
+ val v = LayoutInflater.from(parent.context).inflate(LAYOUT_ID, parent, false)
+
+ return ViewHolder(v)
+ }
+
+ override fun onBindViewHolder(
+ holder: ViewHolder,
+ position: Int
+ ) {
+ val al = alerts[position]
+
+ var til = al.translations.filter { it.field == GtfsAlertsTranslation.FIELD_HEADER && it.language == locale }
+ var text = if(til.isEmpty()) "404" else til[0].text
+ holder.titleTextView.text = text
+
+ til = al.translations.filter { it.field == GtfsAlertsTranslation.FIELD_DESCRIPTION && it.language == locale }
+ text = if(til.isEmpty()) "404" else til[0].text
+ holder.bodyTextView.text = text
+
+ }
+
+ override fun getItemCount(): Int {
+ return alerts.size
+ }
+
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view){
+ val titleTextView: TextView = view.findViewById(R.id.messageTitleTextView)
+ val bodyTextView: TextView = view.findViewById(R.id.messageBodyTextView)
+
+ }
+ companion object{
+ private val LAYOUT_ID = R.layout.entry_alert_line_adapter
+ }
+}
\ No newline at end of file
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,120 @@
+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
+ val dao =gtfsDatabase.alertsDao()
+ //clear old ones
+ dao.deleteOlderThanHours(24)
+
+ 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}")
+ dao.insertMissingAlerts(alertsToAdd, translToAdd, activePeriods, informedEntities)
+
+ 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/AlertsDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/AlertsDao.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/AlertsDao.kt
@@ -0,0 +1,181 @@
+package it.reyboz.bustorino.data.gtfs
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Delete
+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 getAllAlertsLiveData(): LiveData<List<AlertWithDetails>>
+
+ @Transaction
+ @Query("SELECT * FROM gtfsrt_alerts")
+ suspend fun getAllAlerts(): 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)
+
+
+ @Delete
+ suspend fun deleteAlerts(alerts: List<GtfsAlertEntity>)
+ @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
+
+ //TODO use this to remove inactive alerts
+ suspend fun deleteInactiveAlerts() {
+ val alerts = getAllAlerts()
+ val alertsRemove = ArrayList<GtfsAlertEntity>()
+ val currentUnixTime = (System.currentTimeMillis()/1000).toInt()
+ for (a in alerts) {
+ var active = false
+ for(p in a.activePeriods){
+ if(p.end==null || p.start==null) continue
+ if (p.start <= currentUnixTime && p.end>=currentUnixTime) {
+ active = true
+ break
+ }
+ }
+ if(!active)
+ alertsRemove.add(a.alert)
+ }
+ deleteAlerts(alertsRemove)
+ }
+
+ suspend fun deleteOlderThan48h(now: Long = System.currentTimeMillis()): Int {
+ val cutoff = now - 48L * 60L * 60L * 1000L
+ return deleteOlderThan(cutoff)
+ }
+
+ suspend fun deleteOlderThanHours(hours: Long, now : Long = System.currentTimeMillis()): Int {
+ val cutoff = now - hours *60L*60L*1000
+ return deleteOlderThan(cutoff)
+ }
+}
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/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,233 @@
+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 it.reyboz.bustorino.backend.utils
+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")
+ }
+ sb.append("-- Cause: ").append(alert.cause.name).append("\n")
+ sb.append("-- Active periods:\n")
+
+ for(p in activePeriods){
+ if(p.start==null || p.end==null){
+ continue
+ }
+ sb.append("From: ").append(utils.unixTimestampToLocalTime(p.start))
+ sb.append(" to: ").append(utils.unixTimestampToLocalTime(p.end)).append("\n")
+ }
+ val ies = informedEntities
+ sb.append("-- Valid for: \n")
+ 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()
+ }
+
+ fun isActive(unixTimeStamp: Long): Boolean {
+ var active = false
+ for( ac in activePeriods){
+ if(ac.start==null || ac.end == null)
+ continue
+ if (ac.start <= unixTimeStamp && ac.end >= unixTimeStamp) {
+ active = true
+ break
+ }
+ }
+ return active
+ }
+
+
+}
+
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,143 @@
+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.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.cardview.widget.CardView
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.work.ExistingWorkPolicy
+import androidx.work.WorkManager
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.adapters.AlertLineFullAdapter
+import it.reyboz.bustorino.backend.gtfs.GtfsUtils
+import it.reyboz.bustorino.data.GtfsAlertDBDownloadWorker
+import it.reyboz.bustorino.data.gtfs.AlertWithDetails
+import it.reyboz.bustorino.data.gtfs.GtfsAlertsTranslation
+import it.reyboz.bustorino.viewmodels.ServiceAlertsViewModel
+import java.util.Locale
+import kotlin.getValue
+import kotlin.collections.HashMap
+
+
+class AlertsDialogFragment(private val gtfsLineShow: String) : DialogFragment() {
+
+ private lateinit var titleTextView: TextView
+ private lateinit var messageTextView: TextView
+ private lateinit var statusCardView: CardView
+ private lateinit var recyclerView: RecyclerView
+ private val alertsViewModel: ServiceAlertsViewModel by activityViewModels()
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(DEBUG_TAG, "created DialogFragment for line ${gtfsLineShow}")
+ }
+
+ 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)
+ titleTextView.setText(getString(R.string.alert_line_fill,GtfsUtils.lineNameDisplayFromGtfsID(gtfsLineShow)))
+ recyclerView = root.findViewById(R.id.alertsRecyclerView)
+ recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ messageTextView = root.findViewById(R.id.alertMessageTextView)
+ statusCardView = root.findViewById(R.id.statusCard)
+ alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ alerts ->
+ showAlerts(alerts)
+ }
+
+ val btnClose = root.findViewById<ImageButton>(R.id.btnClose)
+ btnClose.setOnClickListener {
+ dismiss()
+ }
+
+ val btnRefresh = root.findViewById<ImageButton>(R.id.btnRefresh)
+ btnRefresh.setOnClickListener {
+ val name = "manualUpdateAlerts"
+ val req = GtfsAlertDBDownloadWorker.makeOneTimeRequest("manualUpdate$gtfsLineShow")
+ WorkManager.getInstance(requireContext()).enqueueUniqueWork(name, ExistingWorkPolicy.KEEP,req)
+ Toast.makeText(context, R.string.checking_alerts_update, Toast.LENGTH_SHORT).show()
+ }
+ return root
+ }
+
+ override fun onStart() {
+ super.onStart()
+ dialog?.window?.setLayout(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ private fun showAlerts(alerts: List<AlertWithDetails>) {
+ val currentLang = Locale.getDefault().language
+ val ms = "language : $currentLang"
+ val langs_msg = HashMap<String, Int>()
+ for (a in alerts) {
+ for (tr in a.translations){
+ if(tr.field == GtfsAlertsTranslation.FIELD_HEADER){
+ tr.language?.let{
+ if(langs_msg.containsKey(it)){
+ langs_msg[it] = langs_msg[it]!! + 1
+ } else{
+ langs_msg[it] = 1
+ }
+ }
+ //found the title, stop
+ break
+ }
+ }
+ }
+ Log.d(DEBUG_TAG, "Lang $currentLang, alerts: $langs_msg, of lang: ${langs_msg[currentLang]}")
+ val msgInLang = langs_msg[currentLang]?: 0
+ val langShow = if (msgInLang > 0){
+ currentLang
+ } else if("en" in langs_msg.keys){
+ "en"
+ } else{
+ "it"
+ } // if there are no messages with "it", then it's over
+ val count = langs_msg[langShow] ?: 0
+ if (count == 0){
+ messageTextView.text = "ERROR: NO ALERTS TO SHOW"
+ statusCardView.visibility = View.VISIBLE
+ } else if(msgInLang == 0){
+ val msgShow = if(langShow == "en") getString(R.string.english) else getString(R.string.italian)
+ messageTextView.text = getString(R.string.no_alerts_in_your_language_fill, msgShow)
+ statusCardView.visibility = View.VISIBLE
+ }
+ // put them in the adapter
+ if(count>0){
+ recyclerView.adapter = AlertLineFullAdapter(alerts, langShow)
+ }
+ }
+
+ 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(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,156 @@
+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.cardview.widget.CardView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.RecyclerView
+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()
+ val unixTimestamp = (System.currentTimeMillis() / 1000)
+ for (x in alerts) {
+ sb.append(x.longPrint())
+ sb.append("----- Alert active: ").append(x.isActive(unixTimestamp)).append("\n\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 {
+ AlertsDialogFragment(lineID).show(parentFragmentManager, "Alerts-Line$lineID")
+ }
/*
*/
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,175 @@
+package it.reyboz.bustorino.viewmodels
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+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.google.transit.realtime.GtfsRealtime.Alert
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.data.GtfsAlertDBDownloadWorker
+import it.reyboz.bustorino.data.GtfsRepository
+import it.reyboz.bustorino.data.gtfs.GtfsDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+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)
+ private val waitingToRerun = AtomicBoolean(false)
+ fun setRunningDownloadRequests(value: Boolean) {
+ keepRunning.set(value)
+ }
+
+ val alertsByRouteLiveData = routeToFilter.switchMap {
+ val unixTimestamp = (System.currentTimeMillis()/1000)
+ gtfsRepo.getAlertsByRouteID(it).map{ l -> l.filter { al->al.isActive(unixTimestamp) }}
+ }
+
+ val alertsByStopLiveData = stopToFilter.switchMap {
+ gtfsRepo.alertsDao.getAlertsForStop(it)
+ }
+
+ val allAlertsLiveData = gtfsRepo.alertsDao.getAllAlertsLiveData()
+ /*
+ 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()
+ waitingToRerun.set(false)
+ 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.postValue(System.currentTimeMillis())
+ }
+ viewModelScope.launch(Dispatchers.IO) {
+ waitingToRerun.set(true)
+ delay((5L*60 +1).seconds)
+ if(keepRunning.get()) downloadWorkIfTimePassed()
+ }
+
+ }
+
+ fun launchAlertsPeriodCheck(){
+ setRunningDownloadRequests(true)
+ if(!waitingToRerun.get())
+ 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/drawable/refresh_line.xml b/app/src/main/res/drawable/refresh_line.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/drawable/refresh_line.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24" android:viewportWidth="24" android:width="32dp">
+
+ <path android:fillColor="?colorPrimary" android:pathData="M5.463,4.433C7.216,2.917 9.501,2 12,2C17.523,2 22,6.477 22,12C22,14.136 21.33,16.116 20.189,17.741L17,12H20C20,7.582 16.418,4 12,4C9.85,4 7.898,4.848 6.46,6.228L5.463,4.433ZM18.537,19.567C16.784,21.083 14.499,22 12,22C6.477,22 2,17.523 2,12C2,9.864 2.67,7.884 3.811,6.259L7,12H4C4,16.418 7.582,20 12,20C14.15,20 16.102,19.152 17.54,17.772L18.537,19.567Z"/>
+
+</vector>
diff --git a/app/src/main/res/layout/entry_alert_line_adapter.xml b/app/src/main/res/layout/entry_alert_line_adapter.xml
new file mode 100644
--- /dev/null
+++ b/app/src/main/res/layout/entry_alert_line_adapter.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView 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"
+ app:cardCornerRadius="5dp"
+ app:cardElevation="5dp"
+ android:layout_marginTop="5dp"
+ android:layout_margin="5dp"
+
+>
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="15dp"
+ >
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+
+ android:id="@+id/messageTitleTextView"
+ android:textStyle="bold"
+ android:textSize="17sp"
+ android:fontFamily="@font/lato_bold"
+ android:textColor="@color/black_900"
+ android:text="Alerts for line"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="5dp"
+ />
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+
+ android:id="@+id/messageBodyTextView"
+ android:textSize="16sp"
+ android:fontFamily="@font/lato_regular"
+ android:textColor="@color/black_900"
+ android:text="Alert text"
+ android:layout_below="@id/messageTitleTextView"
+ />
+
+ </RelativeLayout>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
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,109 @@
+<?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="wrap_content"
+ android:padding="15dp"
+ tools:context=".fragments.AlertsDialogFragment">
+
+ <TextView
+ android:id="@+id/titleTextView"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:fontFamily="@font/pitagon_medium"
+ android:textColor="@color/black_900"
+ android:text="Alerts for line"
+ android:layout_width="wrap_content"
+ android:layout_marginBottom="5dp"
+ android:layout_marginStart="10dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/statusCard"
+ />
+ <androidx.cardview.widget.CardView
+ android:id="@+id/statusCard"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dp"
+
+ android:padding="12dp"
+ app:cardCornerRadius="12dp"
+ app:cardElevation="1dp"
+ android:layout_marginStart="5dp"
+ android:layout_marginEnd="5dp"
+ app:strokeWidth="1dp"
+ android:layout_marginBottom="5dp"
+ android:backgroundTint="@color/red_orange"
+ android:visibility="gone"
+ app:strokeColor="@color/grey_050"
+ app:layout_constraintTop_toBottomOf="@id/titleTextView"
+ app:layout_constraintBottom_toTopOf="@id/alertsRecyclerView"
+ >
+
+ <TextView
+ android:id="@+id/alertMessageTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Messaggio di stato..."
+ android:fontFamily="@font/lato_regular"
+ android:textSize="14sp"
+ android:textColor="@color/white"
+ android:padding="8dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
+
+ </androidx.cardview.widget.CardView>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_max="460dp"
+
+ android:id="@+id/alertsRecyclerView"
+ app:layout_constraintTop_toBottomOf="@id/statusCard"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/btnClose"
+ android:layout_marginBottom="0dp"
+ android:layout_marginTop="5dp"
+ app:fastScrollEnabled="true"
+ app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
+ app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
+ app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
+
+ />
+ <ImageButton
+ android:id="@+id/btnRefresh"
+ android:layout_width="@dimen/dialog_alerts_btn_size"
+ android:layout_height="@dimen/dialog_alerts_btn_size"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ app:layout_constraintTop_toTopOf="@id/btnClose"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:src="@drawable/refresh_line"
+ app:tint="@color/white"
+ android:backgroundTint="?colorPrimaryDark"
+ android:contentDescription="close"
+
+ />
+ <ImageButton
+ android:id="@+id/btnClose"
+ android:layout_width="@dimen/dialog_alerts_btn_size"
+ android:layout_height="@dimen/dialog_alerts_btn_size"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ android:layout_marginTop="8dp"
+ app:layout_constraintTop_toBottomOf="@id/alertsRecyclerView"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:src="@drawable/close_white_large"
+ android:backgroundTint="?colorPrimaryDark"
+ android:contentDescription="close"
+
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ 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,41 @@
android:maxWidth="300sp"
android:layout_marginTop="12dp"
/>
+ <ImageButton
+ android:id="@+id/lineInfoWarningButton"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:scaleType="fitCenter"
+ 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-it/strings.xml b/app/src/main/res/values-it/strings.xml
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -283,4 +283,11 @@
<string name="live_positions_switch_provider">Cambia fonte</string>
<string name="live_positions_preference_switch_clear_title">Rimuovi posizioni sulla mappa quando si cambia fonte delle posizioni in tempo reale</string>
<string name="updated_fill">Aggiornato: %1$s</string>
+
+ <string name="no_alerts_in_your_language_fill">Nessun avviso nella tua lingua, mostrati in %1$s</string>
+ <string name="italian">Italiano</string>
+ <string name="english">Inglese</string>
+ <string name="alert_line_fill">Avvisi per la linea %1$s:</string>
+ <string name="checking_alerts_update">Controllo degli avvisi disponibili in corso</string>
+
</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -13,5 +13,6 @@
<dimen name="subtitle_size">28sp</dimen>
<dimen name="map_toolbar_icon_size">43dp</dimen>
+ <dimen name="dialog_alerts_btn_size">50dp</dimen>
</resources>
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>
@@ -290,7 +292,6 @@
<item>@string/nav_map_text</item>
<item>@string/lines</item>
</array>
-
<string name="positions_source_pref_title">Source of real time positions for buses and trams</string>
<string name="positions_source_mato_descr">MaTO (updated more frequently, might have errors)</string>
<string name="positions_source_gtfsrt_descr">GTFS RT (less frequently updated, more accurate)</string>
@@ -374,8 +375,14 @@
<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>
-
+ <string name="alert_line_fill">Alerts for line %1$s:</string>
+ <string name="no_alerts_in_your_language_fill">No alerts in your language, showing in %1$s</string>
+ <string name="italian">Italian</string>
+ <string name="english">English</string>
+ <string name="checking_alerts_update">Checking new alerts now</string>
</resources>

File Metadata

Mime Type
text/plain
Expires
Wed, May 6, 15:52 (15 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1891200
Default Alt Text
D234.1778075526.diff (80 KB)

Event Timeline