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
@@ -238,7 +238,8 @@
startedFromIntent = tryedFromIntent;
//period database check
- DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false);
+ //DBUpdateCheckWorker.Companion.schedulePeriodicCheck(this,false);
+ DBUpdateCheckWorker.Companion.runDBUpdateCheckWorker(this);
//Watch for database update
DBUpdateWorker.getWorkInfoLiveData(this)
diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java
deleted file mode 100644
--- a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- BusTO (backend components)
- Copyright (C) 2016 Ludovico Pavesi
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
- */
-package it.reyboz.bustorino.adapters;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.widget.ImageView;
-import androidx.annotation.NonNull;
-import androidx.cardview.widget.CardView;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.preference.PreferenceManager;
-
-import android.content.SharedPreferences;
-import android.view.Gravity;
-import android.os.Build;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.PopupMenu;
-import android.widget.TextView;
-
-import java.util.*;
-
-import androidx.recyclerview.widget.RecyclerView;
-import it.reyboz.bustorino.R;
-import it.reyboz.bustorino.backend.Palina;
-import it.reyboz.bustorino.backend.Passaggio;
-import it.reyboz.bustorino.backend.Route;
-import it.reyboz.bustorino.backend.utils;
-import it.reyboz.bustorino.util.PassaggiSorter;
-import it.reyboz.bustorino.util.RouteSorterByArrivalTime;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * This once was a ListView Adapter for BusLine[].
- *
- * Thanks to Framentos developers for the guide:
- * http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/#
- *
- * @author Valerio Bozzolan
- * @author Ludovico Pavesi
- * @author Fabio Mazza
- */
-public class PalinaAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener {
-
- private static final int ROW_LAYOUT = R.layout.entry_bus_line_passage;
- private static final int metroBg = R.drawable.route_background_metro;
- private static final int busBg = R.drawable.route_background_bus;
- private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance;
-
- private static final int busIcon = R.drawable.ic_bus;
- private static final int trainIcon = R.drawable.ic_subway_filled;
- private static final int tramIcon = R.drawable.ic_tram_filled_24;
-
- private final String KEY_CAPITALIZE;
- private Capitalize capit;
-
- private final List mRoutes;
- private final PalinaClickListener mRouteListener;
-
- @NonNull
- @NotNull
- @Override
- public PalinaViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext())
- .inflate(ROW_LAYOUT, parent, false);
- return new PalinaViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(@NonNull @NotNull PalinaViewHolder vh, int position) {
- final Route route = mRoutes.get(position);
- final Context con = vh.itemView.getContext();
- final Resources res = con.getResources();
-
- vh.routeIDTextView.setText(route.getDisplayCode());
- vh.routeCard.setOnClickListener(view -> mRouteListener.requestShowingRoute(route));
-
- // Clicking anywhere on the row shows a popup menu
- vh.itemView.setOnClickListener(view ->
- openPopupMenuDetails(con,view, route)
- ); //vh.rowRouteDestination.getVisibility() == View.VISIBLE ? vh.rowRouteDestination : vh.itemView
-
- if(route.destinazione==null || route.destinazione.length() == 0) {
- vh.rowRouteDestination.setVisibility(View.GONE);
- // move around the route timetable
- final ViewGroup.MarginLayoutParams pars = (ViewGroup.MarginLayoutParams) vh.rowRouteTimetable.getLayoutParams();
- if (pars!=null){
- pars.topMargin = 16;
- if(Build.VERSION.SDK_INT >= 17)
- pars.setMarginStart(20);
- pars.leftMargin = 20;
- }
- } else {
- // View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible.
- vh.rowRouteDestination.setVisibility(View.VISIBLE);
- String dest = route.destinazione;
- switch (capit){
- case ALL:
- dest = route.destinazione.toUpperCase(Locale.ROOT);
- break;
- case FIRST:
- dest = utils.toTitleCase(route.destinazione, true);
- break;
- case DO_NOTHING:
- default:
-
- }
- vh.rowRouteDestination.setText(dest);
- }
- Drawable drawable = null;
- final var resources = con.getResources();
- switch (route.type) {
- //UNKNOWN = BUS for the moment
- case UNKNOWN:
- case BUS:
- default:
- // convertView could contain another background, reset it
- //vh.rowStopIcon.setBackgroundResource(busBg);
- //drawable = ResourcesCompat.getDrawable(con.getResources(), busIcon, con.getTheme());
- //vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
- vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, busIcon, con.getTheme()));
- break;
- case LONG_DISTANCE_BUS:
- //vh.rowStopIcon.setBackgroundResource(extraurbanoBg);
- vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.extraurban_bus_bg, null));
- vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, extraurbanoBg, con.getTheme()));
- break;
- case METRO:
- //vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- //vh.rowStopIcon.setBackgroundResource(metroBg);
- vh.routeIDTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.metro_bg, null));
- // vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
- vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme()));
- break;
- case RAILWAY:
- //vh.rowStopIcon.setBackgroundResource(busBg);
- //vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
- vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme()));
- break;
- case TRAM: // never used but whatever.
- //vh.rowStopIcon.setBackgroundResource(busBg);
- drawable = ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme());
- assert drawable != null;
- drawable.setTint(resources.getColor(R.color.black_icon_text, con.getTheme()) );
- //vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0);
- vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, tramIcon, con.getTheme()));
-
- break;
- }
-
- List passaggi = route.passaggi;
- //TODO: Sort the passaggi with realtime first if source is GTTJSONFetcher
- if(passaggi.size() == 0) {
- vh.rowRouteTimetable.setText(R.string.no_passages);
-
- } else {
- vh.rowRouteTimetable.setText(route.getPassaggiToString());
- }
-
- }
-
- @Override
- public int getItemCount() {
- return mRoutes.size();
- }
-
- //private static final int cityIcon = R.drawable.city;
-
- // hey look, a pattern!
- public static class PalinaViewHolder extends RecyclerView.ViewHolder {
- //final TextView rowStopIcon;
- final TextView routeIDTextView;
- final CardView routeCard;
- final TextView rowRouteDestination;
- final TextView rowRouteTimetable;
- final ImageView busIcon;
-
- public PalinaViewHolder(@NonNull @NotNull View view) {
- super(view);
- routeIDTextView = view.findViewById(R.id.routeNameTextView);
- routeCard = view.findViewById(R.id.routeCard);
- rowRouteDestination = view.findViewById(R.id.routeDestination);
- rowRouteTimetable = view.findViewById(R.id.routesThatStopHere);
- busIcon = view.findViewById(R.id.arrivalsBusIcon);
- }
- }
- private static Capitalize getCapitalize(SharedPreferences shPr, String key){
- String capitalize = shPr.getString(key, "");
-
- switch (capitalize.trim()){
- case "KEEP":
- return Capitalize.DO_NOTHING;
- case "CAPITALIZE_ALL":
- return Capitalize.ALL;
-
- case "CAPITALIZE_FIRST":
- return Capitalize.FIRST;
- }
- return Capitalize.DO_NOTHING;
- }
-
- private void openPopupMenuDetails(Context con, View view, Route route){
- PopupMenu popup = new PopupMenu(con, view, Gravity.END);
- popup.inflate(R.menu.menu_arrivals_line_item);
- if (route.destinazione == null || route.destinazione.isEmpty()) {
- popup.getMenu().findItem(R.id.action_show_direction).setVisible(false);
- }
- popup.setOnMenuItemClickListener(item -> {
- int id = item.getItemId();
- if (id == R.id.action_open_line) {
- mRouteListener.requestShowingRoute(route);
- return true;
- } else if (id == R.id.action_show_direction) {
- mRouteListener.showRouteFullDirection(route);
- return true;
- }
- return false;
- });
- popup.show();
- }
-
- public PalinaAdapter(Context context, Palina p, PalinaClickListener listener, boolean hideEmptyRoutes) {
- Comparator sorter = null;
- if (p.getPassaggiSourceIfAny()== Passaggio.Source.GTTJSON){
- sorter = new PassaggiSorter();
- }
- final List routes;
- if (hideEmptyRoutes){
- // build the routes by filtering them
- routes = new ArrayList<>();
- for(Route r: p.queryAllRoutes()){
- //add only if there is at least one passage
- if (r.numPassaggi()>0){
- routes.add(r);
- }
- }
- } else
- routes = p.queryAllRoutes();
- for(Route r: routes){
- if (sorter==null) Collections.sort(r.passaggi);
- else Collections.sort(r.passaggi, sorter);
- }
-
- Collections.sort(routes,new RouteSorterByArrivalTime());
-
- mRoutes = routes;
- KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit);
- SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context);
- defSharPref.registerOnSharedPreferenceChangeListener(this);
- this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE);
-
- this.mRouteListener = listener;
- }
-
-
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if(key.equals(KEY_CAPITALIZE)){
- capit = getCapitalize(sharedPreferences, KEY_CAPITALIZE);
-
- notifyDataSetChanged();
- }
- }
-
- enum Capitalize{
- DO_NOTHING, ALL, FIRST
- }
-
- public interface PalinaClickListener{
- /**
- * Simple click listener for the whole line (show info)
- * @param route for toast
- */
- void showRouteFullDirection(Route route);
-
- /**
- * Show the line with all the stops in the line screen
- * @param route partial line info
- */
- void requestShowingRoute(Route route);
- }
-}
diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.kt b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.kt
@@ -0,0 +1,315 @@
+/*
+ BusTO (backend components)
+ Copyright (C) 2016 Ludovico Pavesi
+ Copyright (C) 2026 Fabio Mazza
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+package it.reyboz.bustorino.adapters
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import android.view.*
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.ImageView
+import android.widget.PopupMenu
+import android.widget.TextView
+import androidx.cardview.widget.CardView
+import androidx.core.content.res.ResourcesCompat
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.RecyclerView
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaViewHolder
+import it.reyboz.bustorino.backend.Palina
+import it.reyboz.bustorino.backend.Passaggio
+import it.reyboz.bustorino.backend.Route
+import it.reyboz.bustorino.backend.utils
+import it.reyboz.bustorino.data.gtfs.AlertWithDetails
+import it.reyboz.bustorino.util.PassaggiSorter
+import it.reyboz.bustorino.util.RouteSorterByArrivalTime
+import java.util.*
+
+/**
+ * This once was a ListView Adapter for BusLine[].
+ *
+ * Thanks to Framentos developers for the guide:
+ * http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/#
+ *
+ * @author Valerio Bozzolan
+ * @author Ludovico Pavesi
+ * @author Fabio Mazza
+ */
+class PalinaAdapter(context: Context, p: Palina, listener: PalinaClickListener, hideEmptyRoutes: Boolean) :
+ RecyclerView.Adapter(), OnSharedPreferenceChangeListener {
+ private val KEY_CAPITALIZE: String
+ private var capit: Capitalize
+
+ private val mRoutes: MutableList
+ private val mRouteListener: PalinaClickListener
+
+ private var routesWithAlerts = setOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PalinaViewHolder {
+ val view: View = LayoutInflater.from(parent.getContext())
+ .inflate(ROW_LAYOUT, parent, false)
+ return PalinaViewHolder(view)
+ }
+
+ override fun onBindViewHolder(vh: PalinaViewHolder, position: Int) {
+ val route = mRoutes.get(position)
+ val con = vh.itemView.getContext()
+ val res = con.getResources()
+
+ vh.routeIDTextView.setText(route.getDisplayCode())
+ vh.routeCard.setOnClickListener { _ -> mRouteListener.requestShowingRoute(route) }
+
+ // Clicking anywhere on the row shows a popup menu
+ vh.itemView.setOnClickListener(View.OnClickListener { view: View? -> openPopupMenuDetails(con, view, route) }
+ ) //vh.rowRouteDestination.getVisibility() == View.VISIBLE ? vh.rowRouteDestination : vh.itemView
+
+ if (route.destinazione == null || route.destinazione.length == 0) {
+ vh.rowRouteDestination.setVisibility(View.GONE)
+ // move around the route timetable
+ val pars = vh.rowRouteTimetable.getLayoutParams() as MarginLayoutParams?
+ if (pars != null) {
+ pars.topMargin = 16
+ pars.marginStart = 20
+ pars.leftMargin = 20
+ }
+ } else {
+ // View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible.
+ vh.rowRouteDestination.setVisibility(View.VISIBLE)
+ var dest = route.destinazione
+ when (capit) {
+ Capitalize.ALL -> dest = route.destinazione.uppercase()
+ Capitalize.FIRST -> dest = utils.toTitleCase(route.destinazione, true)
+ Capitalize.DO_NOTHING -> {}
+ else -> {}
+ }
+ vh.rowRouteDestination.setText(dest)
+ }
+ var drawable: Drawable? = null
+ val resources = con.getResources()
+ when (route.type) {
+ Route.Type.UNKNOWN, Route.Type.BUS -> // convertView could contain another background, reset it
+ //vh.rowStopIcon.setBackgroundResource(busBg);
+ //drawable = ResourcesCompat.getDrawable(con.getResources(), busIcon, con.getTheme());
+ //vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, busIcon, con.getTheme()))
+
+ Route.Type.LONG_DISTANCE_BUS -> {
+ //vh.rowStopIcon.setBackgroundResource(extraurbanoBg);
+ vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.extraurban_bus_bg, null))
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, extraurbanoBg, con.getTheme()))
+ }
+
+ Route.Type.METRO -> {
+ //vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
+ //vh.rowStopIcon.setBackgroundResource(metroBg);
+ vh.routeIDTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
+ vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.metro_bg, null))
+ // vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme()))
+ }
+
+ Route.Type.RAILWAY ->
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme()))
+
+ Route.Type.TRAM -> {
+ //vh.rowStopIcon.setBackgroundResource(busBg);
+ drawable = ResourcesCompat.getDrawable(resources, trainIcon, con.getTheme())
+ checkNotNull(drawable)
+ drawable.setTint(resources.getColor(R.color.black_icon_text, con.getTheme()))
+ //vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0);
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, tramIcon, con.getTheme()))
+ }
+
+ else ->
+ vh.busIcon.setImageDrawable(ResourcesCompat.getDrawable(resources, busIcon, con.getTheme()))
+ }
+
+ val passaggi = route.passaggi
+ //TODO: Sort the passaggi with realtime first if source is GTTJSONFetcher
+ if (passaggi.size == 0) {
+ vh.rowRouteTimetable.setText(R.string.no_passages)
+ } else {
+ vh.rowRouteTimetable.setText(route.getPassaggiToString())
+ }
+
+ if(route.gtfsId in routesWithAlerts){
+ vh.alertIcon.visibility = View.VISIBLE
+ vh.alertIcon.setOnClickListener { mRouteListener.requestShowAlertsRoute(route) }
+ } else{
+ vh.alertIcon.visibility = View.GONE
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return mRoutes.size
+ }
+
+ //private static final int cityIcon = R.drawable.city;
+ // hey look, a pattern!
+ class PalinaViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ //final TextView rowStopIcon;
+ val routeIDTextView: TextView = view.findViewById(R.id.routeNameTextView)
+ val routeCard: CardView = view.findViewById(R.id.routeCard)
+ val rowRouteDestination: TextView = view.findViewById(R.id.routeDestination)
+ val rowRouteTimetable: TextView = view.findViewById(R.id.routesThatStopHere)
+ val busIcon: ImageView = view.findViewById(R.id.arrivalsBusIcon)
+
+ val alertIcon : ImageView = view.findViewById(R.id.bubbleImageView)
+ }
+
+ private fun openPopupMenuDetails(con: Context?, view: View?, route: Route) {
+ val popup = PopupMenu(con, view, Gravity.END)
+ popup.inflate(R.menu.menu_arrivals_line_item)
+ if(route.gtfsId !in routesWithAlerts){
+ popup.menu.findItem(R.id.action_show_alerts_line).isVisible = false
+ }
+ if (route.destinazione == null || route.destinazione.isEmpty()) {
+ popup.getMenu().findItem(R.id.action_show_direction).setVisible(false)
+ }
+ popup.setOnMenuItemClickListener{ item: MenuItem? ->
+ val id = item!!.getItemId()
+ when (id) {
+ R.id.action_open_line -> {
+ mRouteListener.requestShowingRoute(route)
+ return@setOnMenuItemClickListener true
+ }
+ R.id.action_show_direction -> {
+ mRouteListener.showRouteFullDirection(route)
+ return@setOnMenuItemClickListener true
+ }
+ R.id.action_show_alerts_line -> {
+ mRouteListener.requestShowAlertsRoute(route)
+ return@setOnMenuItemClickListener true
+ }
+ else -> false //return keyword is not needed
+ }
+ }
+ popup.show()
+ }
+
+ init {
+ var sorter: Comparator? = null
+ if (p.getPassaggiSourceIfAny() == Passaggio.Source.GTTJSON) {
+ sorter = PassaggiSorter()
+ }
+ val routes: MutableList
+ if (hideEmptyRoutes) {
+ // build the routes by filtering them
+ routes = ArrayList()
+ for (r in p.queryAllRoutes()) {
+ //add only if there is at least one passage
+ if (r.numPassaggi() > 0) {
+ routes.add(r)
+ }
+ }
+ } else routes = p.queryAllRoutes()
+ for (r in routes) {
+ if (sorter == null) Collections.sort(r.passaggi)
+ else Collections.sort(r.passaggi, sorter)
+ }
+
+ Collections.sort(routes, RouteSorterByArrivalTime())
+
+ mRoutes = routes
+ KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit)
+ val defSharPref = PreferenceManager.getDefaultSharedPreferences(context)
+ defSharPref.registerOnSharedPreferenceChangeListener(this)
+ this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE)
+
+ this.mRouteListener = listener
+ }
+
+ fun setAlerts(alerts: List){
+ val routeAlerts = mutableSetOf()
+
+ for(a in alerts){
+ for(ent in a.informedEntities){
+ if(ent.routeId == null)
+ continue
+ for(r in mRoutes){
+ val gtfsId = r.gtfsId
+ if(gtfsId != null && gtfsId == ent.routeId){
+ routeAlerts.add(gtfsId)
+ break
+ }
+ }
+ }
+ }
+ if(routeAlerts.isNotEmpty() && routeAlerts !=routesWithAlerts){
+ routesWithAlerts = routeAlerts.toSet()
+ notifyDataSetChanged()
+ }
+ }
+
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
+ if (key == KEY_CAPITALIZE) {
+ val capitNew = getCapitalize(sharedPreferences, KEY_CAPITALIZE)
+ if(capitNew!=capit) {
+ notifyDataSetChanged()
+ capit = capitNew
+ }
+ }
+ }
+
+ internal enum class Capitalize {
+ DO_NOTHING, ALL, FIRST
+ }
+
+ interface PalinaClickListener {
+ /**
+ * Simple click listener for the whole line (show info)
+ * @param route for toast
+ */
+ fun showRouteFullDirection(route: Route)
+
+ /**
+ * Show the line with all the stops in the line screen
+ * @param route partial line info
+ */
+ fun requestShowingRoute(route: Route)
+
+ fun requestShowAlertsRoute(route: Route)
+ }
+
+ companion object {
+ private val ROW_LAYOUT = R.layout.entry_bus_line_passage
+ private val metroBg = R.drawable.route_background_metro
+ private val busBg = R.drawable.route_background_bus
+ private val extraurbanoBg = R.drawable.route_background_bus_long_distance
+
+ private val busIcon = R.drawable.ic_bus
+ private val trainIcon = R.drawable.ic_subway_filled
+ private val tramIcon = R.drawable.ic_tram_filled_24
+
+ private fun getCapitalize(shPr: SharedPreferences, key: String?): Capitalize {
+ val capitalize: String = shPr.getString(key, "")!!
+
+ when (capitalize.trim()) {
+ "KEEP" -> return Capitalize.DO_NOTHING
+ "CAPITALIZE_ALL" -> return Capitalize.ALL
+
+ "CAPITALIZE_FIRST" -> return Capitalize.FIRST
+ }
+ return Capitalize.DO_NOTHING
+ }
+ }
+}
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java
--- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java
@@ -34,7 +34,9 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.net.URI;
import java.text.SimpleDateFormat;
+import java.time.OffsetDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -411,4 +413,17 @@
return new BigDecimal(value).setScale(decimalPlace,
RoundingMode.HALF_UP).stripTrailingZeros().doubleValue();
}
+
+ public static long parseFullDateToEpochMilliseconds(String date){
+ return OffsetDateTime.parse(date).toInstant().toEpochMilli();
+ }
+
+ public boolean isValidUrl(String url) {
+ try {
+ URI uri = new URI(url);
+ return uri.getScheme() != null && uri.getHost() != null;
+ } catch (Exception e) {
+ return false;
+ }
+ }
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateCheckWorker.kt
@@ -20,9 +20,25 @@
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
+import androidx.core.app.NotificationCompat
import androidx.work.*
+import com.android.volley.Response
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.HttpHeaderParser
+import com.android.volley.toolbox.JsonRequest
+import com.android.volley.toolbox.RequestFuture
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.utils
import it.reyboz.bustorino.data.gtfs.GtfsDatabase
+import it.reyboz.bustorino.data.gtfs.GtfsStop
+import it.reyboz.bustorino.data.gtfs.GtfsTableJsonInserter
+import org.json.JSONObject
import java.util.concurrent.TimeUnit
+import androidx.core.content.edit
+import androidx.work.WorkManager.Companion.getInstance
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.backend.Notifications
+import it.reyboz.bustorino.data.gtfs.GtfsTableJsonInserter.Companion.parseURLFromPreferences
/**
* Lightweight periodic worker that checks local state and enqueues [DBUpdateWorker]
@@ -39,14 +55,15 @@
val lastDBUpdateTime = sharedPrefs.getLong(PreferencesHolder.DB_LAST_UPDATE_KEY, 0L)
val currentTime = System.currentTimeMillis() / 1000
- val neverUpdated = currentDBVersion < 0 || lastDBUpdateTime <= 0
+
+ val neverUpdated = lastDBUpdateTime <= 0
val timeElapsed = currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY
val isSpecialCase = checkIfNeedSpecialUpgradeDB(con)
-
+ //Last launch the DB update from MaTO
if (neverUpdated || timeElapsed || isSpecialCase) {
- Log.d(DEBUG_TAG, "Scheduling DBUpdateWorker")
+ Log.d(DEBUG_TAG, "Scheduling DBUpdateWorker, never updated: $neverUpdated, timeElapsed : $timeElapsed")
DBUpdateWorker.requestDBUpdateUniqueWork(con, forced = true)
if(isSpecialCase){
val gtfsDBVer = getGtfsDBVersion(con)
@@ -58,15 +75,96 @@
Log.d(DEBUG_TAG, "No update needed")
}
+ //// FIRST, RUN CHECK FOR GTFS DATA
+ val lastGTFSDatabaseUpdateTime = sharedPrefs.getLong(PreferencesHolder.DB_GTFS_LAST_UPDATE_STOPS, 0L)
+ val newGtfsUpdateTime = checkGtfsVersion("stops")
+ Log.d(DEBUG_TAG, "check for stops version: $lastGTFSDatabaseUpdateTime, new is $newGtfsUpdateTime")
+
+ if(newGtfsUpdateTime > lastGTFSDatabaseUpdateTime) {
+ Log.d(DEBUG_TAG, "Updating GTFS data, tables are more recent")
+ val downloader = GtfsTableJsonInserter(applicationContext, "stops", GtfsStop.Companion)
+
+ val done = downloader.downloadAndInsertStops()
+ if(done){
+ sharedPrefs.edit { putLong(PreferencesHolder.DB_GTFS_LAST_UPDATE_STOPS, newGtfsUpdateTime) }
+ }
+ Log.d(DEBUG_TAG, "Completed download, insert worked: $done")
+ }
+
return Result.success()
}
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val context = applicationContext
+ Notifications.createDBNotificationChannelIfNeeded(context)
+
+ val builder = NotificationCompat.Builder(
+ context,
+ Notifications.DB_UPDATE_CHANNELS_ID
+ )
+ .setContentTitle(context.getString(it.reyboz.bustorino.R.string.database_update_msg_notif))
+ .setProgress(0, 0, true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ builder.setSmallIcon(R.drawable.refresh_line)
+
+ return ForegroundInfo(NOTIFICATION_ID, builder.build())
+ }
+
+
+ fun checkGtfsVersion(table: String): Long {
+ val volleyManager = NetworkVolleyManager.getInstance(applicationContext)
+ val errorListener = Response.ErrorListener { error ->
+ Log.w(DEBUG_TAG, "Error fetching gtfs version",error)
+ }
+ var version: Long = 0
+ val future = RequestFuture.newFuture()
+ //response ->
+ //}
+ var url = parseURLFromPreferences(applicationContext)
+ if (url.last() != '/')
+ url+="/"
+ url += "versions.json"
+
+ val req = object : JsonRequest(Method.GET, url, null, future, errorListener) {
+ override fun parseNetworkResponse(p0: com.android.volley.NetworkResponse): Response {
+ var response:Long = 0
+ try{
+ val data = JSONObject(String(p0.data))
+ val selobj = data.getJSONObject("$table.txt")
+ val lastDate = selobj.getString("last_modified")
+
+ response = utils.parseFullDateToEpochMilliseconds(lastDate)
+ } catch (e: Exception){
+ //Log.w(DEBUG_TAG, "Error parsing network response",e)
+ return Response.error(VolleyError(e))
+ }
+
+ return Response.success(response, HttpHeaderParser.parseCacheHeaders(p0))
+ }
+ }
+ volleyManager.requestQueue.add(req)
+
+ try {
+ version = future.get(30, TimeUnit.SECONDS)
+ }catch (e: Exception){
+ e.printStackTrace()
+ }
+ finally {
+ future.cancel(true)
+ }
+ return version
+ }
+
companion object {
const val DEBUG_TAG = "BusTO-DBUpdateScheduler"
const val WORK_NAME = "DBUpdateChecker"
+ //const val URL_JSON_GTFS_VERSIONS = "https://github.com/fabmazz/gtfs_gtt_check/releases/latest/download/versions.json"
+
+ private const val NOTIFICATION_ID = 328918201
+
private const val UPDATE_MIN_DELAY = (3 * 24 * 3600L) //
fun schedulePeriodicCheck(context: Context, restart: Boolean = false) {
@@ -87,6 +185,20 @@
.enqueueUniquePeriodicWork(WORK_NAME, policy, workRequest)
}
+ fun runDBUpdateCheckWorker(context: Context) {
+ val workManager = getInstance(context)
+
+ val wr = OneTimeWorkRequest.Builder(DBUpdateCheckWorker::class.java)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 20, TimeUnit.SECONDS)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .setConstraints(
+ Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ )
+ .build()
+
+ workManager.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, wr)
+ }
@JvmStatic
private fun getGtfsDBVersion(context: Context): Int {
val gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(context)
@@ -103,10 +215,7 @@
//applicationContext.getMainSharedPreferences()
val old_version = PreferencesHolder.getGtfsDBVersion(theShPr)
- Log.d(
- DEBUG_TAG,
- "GTFS Database: old version is $old_version, new version is $db_version"
- )
+
if (old_version < db_version) {
//decide update conditions in the future
if (old_version < 2 && db_version >= 2) {
@@ -115,6 +224,7 @@
}
//PreferencesHolder.setGtfsDBVersion(theShPr, db_version)
}
+ Log.d(DEBUG_TAG,"Special update needed: $dataUpdateNeeded, old version: $old_version, new version: $db_version")
return dataUpdateNeeded
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.kt
@@ -30,6 +30,7 @@
import it.reyboz.bustorino.backend.Notifications
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
+import androidx.core.content.edit
/**
* Worker class that runs the DB update, without checking if it is needed or not
@@ -89,11 +90,11 @@
}
Log.d(DEBUG_TAG, "Update finished successfully!")
//update the version in the shared preference
- val editor = sharedPrefs.edit()
- editor.putInt(PreferencesHolder.DB_GTT_VERSION_KEY, newDBVersion)
- val currentTime = System.currentTimeMillis() / 1000
- editor.putLong(PreferencesHolder.DB_LAST_UPDATE_KEY, currentTime)
- editor.apply()
+ sharedPrefs.edit {
+ putInt(PreferencesHolder.DB_GTT_VERSION_KEY, newDBVersion)
+ val currentTime = System.currentTimeMillis() / 1000
+ putLong(PreferencesHolder.DB_LAST_UPDATE_KEY, currentTime)
+ }
//cancelNotification(NOTIFICATION_ID)
return Result.success(Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build())
diff --git a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
deleted file mode 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- BusTO - Data components
- Copyright (C) 2021 Fabio Mazza
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
- */
-package it.reyboz.bustorino.data;
-
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.DatabaseErrorHandler;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import androidx.lifecycle.LiveData;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import it.reyboz.bustorino.BuildConfig;
-import it.reyboz.bustorino.backend.Stop;
-
-public class FavoritesLiveData extends LiveData> implements CustomAsyncQueryHandler.AsyncQueryListener {
- private static final String TAG = "BusTO-FavoritesLiveData";
- private final boolean notifyChangesDescendants;
-
-
- @NonNull
- private final Context mContext;
-
- @NonNull
- private final FavoritesLiveData.ForceLoadContentObserver mObserver;
- private final CustomAsyncQueryHandler queryHandler;
-
-
- private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
- AppDataProvider.FAVORITES).build();
-
-
- private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=220;
-
-
- @Nullable
- private List stopsFromFavorites, stopsDone;
-
- private boolean isQueryRunning = false;
- private int stopNeededCount = 0;
-
- public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) {
- super();
- mContext = context.getApplicationContext();
- mObserver = new FavoritesLiveData.ForceLoadContentObserver();
- notifyChangesDescendants = notifyDescendantsChanges;
- queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this);
-
- }
-
- public void loadData() {
- loadData(false);
- }
- private static Uri.Builder getStopsBuilder(){
- return AppDataProvider.getUriBuilderToComplete().appendPath("stop");
-
- }
-
- private void loadData(boolean forceQuery) {
- Log.d(TAG, "loadData() force: "+forceQuery);
-
- if (!forceQuery){
- if (getValue()!= null){
- //Data already loaded
- Log.d(TAG, "Data already loaded");
- return;
- }
- }
- if (isQueryRunning){
- //we are waiting for data, we will get an update soon
- Log.d(TAG, "Query is running, abort");
- return;
- }
-
- isQueryRunning = true;
- startQuery();
-
- }
-
- private void startQuery(){
- Log.d(TAG, "startQuery for token "+FAV_TOKEN);
- queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.FAVORITES_COLUMNS_ARRAY, null, null, null);
-
- }
-
- public void stopQuery(){
- queryHandler.cancelOperation(FAV_TOKEN);
- isQueryRunning = false;
- }
-
- public void forceReload(){
- loadData(true);
- }
-
- @Override
- protected void onActive() {
- //Log.d(TAG, "onActive()");
- loadData(true);
- }
-
- /**
- * Clear the data for the cursor
- */
- public void onClear(){
-
- ContentResolver resolver = mContext.getContentResolver();
- resolver.unregisterContentObserver(mObserver);
-
- }
-
-
- @Override
- protected void setValue(List stops) {
- //Log.d("BusTO-FavoritesLiveData","Setting the new values for the stops, have "+
- // stops.size()+" stops");
-
- ContentResolver resolver = mContext.getContentResolver();
- resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver);
-
- super.setValue(stops);
- }
-
- @Override
- public void onQueryComplete(int token, Object cookie, Cursor cursor) {
- if (cursor == null){
- Log.e(TAG, "Null cursor for token "+token);
- if(token == FAV_TOKEN){
- //restart query
- Log.d(TAG, "Restarting query");
- queryHandler.cancelOperation(FAV_TOKEN);
-
- isQueryRunning = false;
- loadData(true);
- }
- return;
- }
- if (token == FAV_TOKEN) {
- stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.FAVORITES_COLUMNS_ARRAY);
- cursor.close();
- //reset counters
- stopNeededCount = stopsFromFavorites.size();
- stopsDone = new ArrayList<>();
- if(stopsFromFavorites.isEmpty()){
- //we don't need to call the other query
- setValue(stopsDone);
- isQueryRunning = false;
- } else
- for (int i = 0; i < stopsFromFavorites.size(); i++) {
- Stop s = stopsFromFavorites.get(i);
- queryHandler.startQuery(STOPS_TOKEN_BASE + i, null,
- getStopsBuilder().appendPath(s.ID).build(),
- NextGenDB.QUERY_COLUMN_stops_all, null, null, null);
- }
-
-
-
- } else if(token >= STOPS_TOKEN_BASE){
- final int index = token - STOPS_TOKEN_BASE;
- assert stopsFromFavorites != null;
- Stop stopUpdate = stopsFromFavorites.get(index);
- Stop finalStop;
-
- List result = NextGenDB.getStopsFromCursorAllFields(cursor);
- cursor.close();
- if (result.isEmpty()){
- // stop is not in the DB
- finalStop = stopUpdate;
- } else {
- finalStop = result.get(0);
- if (BuildConfig.DEBUG && !(finalStop.ID.equals(stopUpdate.ID))) {
- throw new AssertionError("Assertion failed");
- }
- finalStop.setStopUserName(stopUpdate.getStopUserName());
- }
- if (stopsDone!=null)
- stopsDone.add(finalStop);
-
- stopNeededCount--;
- if (stopNeededCount == 0) {
- // we have finished the queries
- isQueryRunning = false;
- Collections.sort(stopsDone);
-
- setValue(stopsDone);
- }
-
- }
- }
-
-
- /**
- * Content Observer that forces reload of cursor when data changes
- * On different thread (new Handler)
- */
- public final class ForceLoadContentObserver
- extends ContentObserver {
-
- public ForceLoadContentObserver() {
- super(new Handler(Looper.myLooper()));
- }
-
- @Override
- public boolean deliverSelfNotifications() {
- return true;
- }
-
- @Override
- public void onChange(boolean selfChange) {
- Log.d(TAG, "ForceLoadContentObserver.onChange()");
- loadData(true);
- }
-
- }
-
-
-}
-
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
@@ -42,7 +42,7 @@
return gtfsDao.getAllRoutes()
}
- fun getRouteFromGtfsId(gtfsId: String): LiveData{
+ fun getRouteFromGtfsId(gtfsId: String): LiveData{
return gtfsDao.getRouteByGtfsID(gtfsId)
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
--- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java
@@ -42,6 +42,7 @@
public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run";
public static final String DB_GTT_VERSION_KEY = "NextGenDB.GTTVersion";
public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate";
+ public static final String DB_GTFS_LAST_UPDATE_STOPS = "GtfsDatabase.LastUpdate.Stops";
public static final String PREF_FAVORITE_LINES = "pref_favorite_lines";
// match the one in preferences.xml
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
@@ -27,7 +27,13 @@
fun getAllRoutes() : LiveData>
@Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} LIKE :gtfsId")
- fun getRouteByGtfsID(gtfsId: String) : LiveData
+ fun getRouteByGtfsID(gtfsId: String) : LiveData
+
+ @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_SHORT_NAME} LIKE :routeId")
+ fun getRouteByShortName(routeId: String) : LiveData
+
+ @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_SHORT_NAME} IN (:shortNames)")
+ fun getRoutesFromShortNames(shortNames: List) : LiveData>
@Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE)
@@ -36,8 +42,11 @@
@Query("SELECT "+GtfsStop.COL_STOP_ID+" FROM "+GtfsStop.DB_TABLE)
fun getAllStopsIDs() : List
- @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID")
- fun getStopByStopID(queryID: String): LiveData>
+ @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" = :queryID")
+ fun getStopByStopCode(queryID: String): LiveData
+
+ //@Query("SELECT * FROM ${GtfsStop.DB_TABLE} WHERE ${GtfsStop.COL_STOP_ID} LIKE :stopID")
+ //fun getStopByGtfsID(stopID: Int) : LiveData
@Query("SELECT * FROM "+GtfsShape.DB_TABLE+
" WHERE "+GtfsShape.COL_SHAPE_ID+" LIKE :shapeID"+
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt
@@ -28,7 +28,7 @@
val gtfsId: String,
@ColumnInfo(name = COL_AGENCY_ID)
val agencyID: String,
- @ColumnInfo(name = "route_short_name")
+ @ColumnInfo(name = COL_SHORT_NAME)
val shortName: String,
@ColumnInfo(name = "route_long_name")
val longName: String,
@@ -62,11 +62,12 @@
const val COL_ROUTE_ID = "route_id"
const val COL_MODE ="route_mode"
const val COL_COLOR="route_color"
+ const val COL_SHORT_NAME ="route_short_name"
const val COL_TEXT_COLOR="route_text_color"
val COLUMNS = arrayOf(COL_ROUTE_ID,
COL_AGENCY_ID,
- "route_short_name",
+ COL_SHORT_NAME,
"route_long_name",
"route_desc",
"route_type",
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsStop.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
@@ -17,9 +17,14 @@
*/
package it.reyboz.bustorino.data.gtfs
+import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
+import de.siegmar.fastcsv.reader.CsvRecord
+import de.siegmar.fastcsv.reader.NamedCsvRecord
+import it.reyboz.bustorino.data.GtfsRepository
+import it.reyboz.bustorino.data.gtfs.GtfsStop
@Entity(tableName = GtfsStop.DB_TABLE)
data class GtfsStop(
@@ -27,7 +32,7 @@
@ColumnInfo(name= COL_STOP_ID)
val internalID: Int,
@ColumnInfo(name= COL_STOP_CODE)
- val gttStopID: String,
+ val stopCode: String,
@ColumnInfo(name= COL_STOP_NAME)
val stopName: String,
@ColumnInfo(name= COL_GTT_PLACE)
@@ -52,7 +57,8 @@
//valuesByColumn["zone_id"]?.toIntOrNull()!!,
Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR])
)
- companion object{
+
+ companion object : RecordInserter {
const val DB_TABLE="stops_gtfs"
const val COL_STOP_CODE="stop_code"
const val COL_STOP_ID = "stop_id"
@@ -71,6 +77,25 @@
//"zone_id",
COL_WHEELCHAIR
)
+
+ override fun fromRecord(record: NamedCsvRecord): GtfsStop {
+ return GtfsStop(record.getField(COL_STOP_ID).toInt(),
+ record.getField(COL_STOP_CODE),
+ record.getField(COL_STOP_NAME),
+ record.getField(COL_GTT_PLACE),
+ record.getField(COL_LATITUDE).toDouble(),
+ record.getField(COL_LONGITUDE).toDouble(),
+ Converters.wheelchairFromString(record.getField(COL_WHEELCHAIR))
+ )
+ }
+
+ override fun insertInDatabase(
+ elements: List,
+ context: Context
+ ): Boolean {
+ GtfsRepository(context).gtfsDao.insertStops(elements)
+ return true
+ }
}
override fun getColumns(): Array {
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTable.java b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTable.java
--- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTable.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTable.java
@@ -20,5 +20,4 @@
public interface GtfsTable {
String[] getColumns();
-
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTableJsonInserter.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTableJsonInserter.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTableJsonInserter.kt
@@ -0,0 +1,95 @@
+package it.reyboz.bustorino.data.gtfs
+
+import android.content.Context
+import android.util.Log
+import com.android.volley.NetworkResponse
+import com.android.volley.Request.Method
+import com.android.volley.Response
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.HttpHeaderParser
+import com.android.volley.toolbox.JsonRequest
+import com.android.volley.toolbox.RequestFuture
+import de.siegmar.fastcsv.reader.CsvReader
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.Result
+import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.GtfsRepository
+import it.reyboz.bustorino.data.PreferencesHolder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import java.io.ByteArrayInputStream
+import java.util.concurrent.TimeUnit
+import java.util.zip.GZIPInputStream
+
+class GtfsTableJsonInserter(
+ context: Context,
+ //callback: Result.Callback
+ val tableName: String,
+ val inserter: RecordInserter
+) {
+ private val context = context.applicationContext
+
+ fun downloadAndInsertStops(): Boolean {
+ val volleyManager = NetworkVolleyManager.getInstance(context)
+ var url = parseURLFromPreferences(context)
+ if (url.last() != '/')
+ url+="/"
+ url += "$tableName.txt.gz"
+ val errorListener = Response.ErrorListener { error ->
+ Log.w("BusTO-GtfsTableInsert", "Error fetching table stops, url $url ",error)
+ }
+ val future = RequestFuture.newFuture>()
+ //response ->
+ //}
+ val req = object : JsonRequest>(Method.GET,url,null, future, errorListener) {
+ override fun parseNetworkResponse(p0: NetworkResponse): Response> {
+ var stops: List
+ try{
+ val input = GZIPInputStream(ByteArrayInputStream(p0.data))
+
+ val reader = CsvReader.builder().ofNamedCsvRecord(input)
+
+ stops = reader.map { record ->
+ inserter.fromRecord(record)
+ }
+
+ } catch (e: Exception){
+ //Log.w(DEBUG_TAG, "Error parsing network response",e)
+ return Response.error(VolleyError(e))
+ }
+
+ return Response.success(stops, HttpHeaderParser.parseCacheHeaders(p0))
+ }
+ }
+ volleyManager.requestQueue.add(req)
+
+ try {
+ val stops = future.get(30, TimeUnit.SECONDS)
+
+ inserter.insertInDatabase(stops, context)
+ }catch (e: Exception){
+ e.printStackTrace()
+ return false
+ }
+ finally {
+ future.cancel(true)
+ }
+ return true
+ }
+
+ companion object {
+ //this is used
+ const val BASE_URL = "https://github.com/fabmazz/gtfs_gtt_check/releases/latest/download"
+
+ fun parseURLFromPreferences(context: Context): String {
+ val pref = PreferencesHolder.getAppPreferences(context)
+ // THIS HAS TO MATCH xml/preferences.xml
+ val urlPrefs = pref.getString("pref_url_gtfs_data", "")
+ Log.d("BusTO-DataDownload", "Base url for GTFS data from preferences : $urlPrefs")
+ if(urlPrefs.isNullOrEmpty()){
+ return BASE_URL
+ } else return urlPrefs
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/RecordInserter.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/RecordInserter.kt
new file mode 100644
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/RecordInserter.kt
@@ -0,0 +1,11 @@
+package it.reyboz.bustorino.data.gtfs
+
+import android.content.Context
+import de.siegmar.fastcsv.reader.NamedCsvRecord
+
+interface RecordInserter {
+
+ fun fromRecord(record: NamedCsvRecord): T
+
+ fun insertInDatabase(elements: List, context: Context): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/AlertsDialogFragment.kt
@@ -78,13 +78,17 @@
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
messageTextView = root.findViewById(R.id.alertMessageTextView)
statusCardView = root.findViewById(R.id.statusCard)
- if(gtfsLineShow.isNotEmpty())
+ if(stopToShow.isNotEmpty()){
+ if(gtfsLineShow.isNotEmpty()){
+ alertsViewModel.alertsByStopLineLiveData.observe(viewLifecycleOwner) { showAlerts(it)}
+ } else
+ alertsViewModel.alertsByStopLiveData.observe(viewLifecycleOwner){ showAlerts(it)}
+ }
+ else if(gtfsLineShow.isNotEmpty())
alertsViewModel.alertsByRouteLiveData.observe(viewLifecycleOwner){ alerts ->
showAlerts(alerts)
}
- else if(stopToShow.isNotEmpty()){
- alertsViewModel.alertsByStopLiveData.observe(viewLifecycleOwner){ alerts -> showAlerts(alerts) }
- }
+
val btnClose = root.findViewById(R.id.btnClose)
btnClose.setOnClickListener {
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt
@@ -120,6 +120,16 @@
override fun requestShowingRoute(route: Route) {
showRoutesInLinesFragment(route)
}
+
+ override fun requestShowAlertsRoute(route: Route) {
+ val gtfsId = route.gtfsId
+ if(gtfsId == null) return
+
+ alertsViewModel.setGtfsLineFilter(gtfsId)
+ val stopID = lastUpdatedPalina!!.ID
+ AlertsDialogFragment(gtfsId,lastUpdatedPalina!!.ID).show(
+ parentFragmentManager,"Alerts_stop${stopID}_line${gtfsId}")
+ }
}
private fun showRoutesInLinesFragment(route: Route) {
@@ -342,7 +352,7 @@
})
alertsViewModel.alertsByStopLiveData.observe(viewLifecycleOwner) { alerts ->
- if(alerts!=null && alerts.isNotEmpty()){
+ /*if(alerts!=null && alerts.isNotEmpty()){
alertsCardView.visibility = View.VISIBLE
alertsCardView.setOnClickListener {
AlertsDialogFragment.newInstanceForStop(stopID).show(parentFragmentManager, "AlertsDialogStop$stopID")
@@ -350,6 +360,10 @@
} else{
alertsCardView.visibility = View.GONE
}
+ */
+ mListAdapter?.apply{
+ setAlerts(alerts)
+ }
}
}
@@ -516,7 +530,7 @@
lastUpdatedPalina = p
//set the gtfsID for the alerts
if(isAdded){
- //alertsViewModel.setStopFilter(p)
+ alertsViewModel.setStopFilter(p)
} else{
Log.w(DEBUG_TAG, "Cannot filter alerts for palina $p, the fragment is not added")
}
@@ -534,7 +548,10 @@
updateMessage()
}
- val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true)
+ val adapter = PalinaAdapter(requireContext(), lastUpdatedPalina!!, palinaClickListener, true)
+ alertsViewModel.alertsByStopLiveData.value?.let{
+ adapter.setAlerts(it)
+ }
p?.let {
//only update the sources if we have actual passaggi
if (arrivalsViewModel.arrivalsRequestRunningLiveData.value == false)
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt
--- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt
@@ -318,6 +318,13 @@
//showRecyclerHidingLoadMessage()
//if (mListener != null) mListener!!.readyGUIfor(FragmentKind.NEARBY_ARRIVALS)
}
+ /*viewModel.locationLiveData.observe(getViewLifecycleOwner()) {loc ->
+ if(loc!=null){
+ setShowingStatus(LocationShowingStatus.LOCATION_FOUND)
+ }
+ }
+
+ */
//added
//checkPermissionLocationStart()
@@ -407,9 +414,11 @@
private fun setShowingStatus(newStatus: LocationShowingStatus) {
var newStatus = newStatus
- if (newStatus == showingStatus) {
+ /*if (newStatus == showingStatus) {
return
}
+
+ */
if (BuildConfig.DEBUG) Log.d(DEBUG_TAG, "Changing showing status from $showingStatus to $newStatus")
if (!isLocationEnabled && newStatus != LocationShowingStatus.NO_PERMISSION) {
@@ -490,7 +499,10 @@
loadPreferencesStops()
setGuiForFragmentType(fragmentType)
//if(lastPosition == null){
- viewModel.locationLiveData.value?.let{loc -> lastPosition = loc }
+ viewModel.locationLiveData.value?.let{loc ->
+ lastPosition = loc
+ setShowingStatus(LocationShowingStatus.LOCATION_FOUND)
+ }
//}
if(bothLocationPermissionsGranted(requireContext())){
@@ -498,9 +510,6 @@
if(isLocationEnabled()){
//location is enabled, start updates
startLocationUpdatesByType()
- if(lastPosition == null){
- setShowingStatus(LocationShowingStatus.SEARCHING)
- }
} else{
setShowingStatus(LocationShowingStatus.DISABLED)
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
--- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java
@@ -37,6 +37,7 @@
import androidx.work.WorkManager;
import it.reyboz.bustorino.ActivityBackup;
import it.reyboz.bustorino.R;
+import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.data.DBUpdateWorker;
import it.reyboz.bustorino.data.GtfsMaintenanceWorker;
import it.reyboz.bustorino.data.PreferencesHolder;
@@ -119,6 +120,10 @@
}
}
);
+ final EditTextPreference editPref = findPreference("pref_url_gtfs_data");
+ if(editPref!=null){
+ editPref.setDialogMessage(utils.convertHtml(getString(R.string.pref_URL_gtfs_data_message)));
+ }
else {
Log.e("BusTO-Preferences", "Cannot find db update preference");
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
@@ -28,13 +28,16 @@
import androidx.lifecycle.viewModelScope
import it.reyboz.bustorino.backend.*
import it.reyboz.bustorino.backend.mato.MatoAPIFetcher
+import it.reyboz.bustorino.data.GtfsRepository
import it.reyboz.bustorino.data.NextGenDB
import it.reyboz.bustorino.data.OldDataRepository
+import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.middleware.RecursionHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
+import kotlin.collections.firstOrNull
class ArrivalsViewModel(application: Application): AndroidViewModel(application) {
@@ -43,8 +46,15 @@
private val executor = Executors.newFixedThreadPool(2)
private val oldRepo = OldDataRepository(executor, application)
+ private val gtfsRepo = GtfsRepository(application)
val palinaFromArrivals = MediatorLiveData()
+
+ val gtfsRoutesArrivals = palinaFromArrivals.switchMap { p ->
+ val names = p.queryAllRoutes().map{ r-> r.name}
+ Log.d(DEBUG_TAG, "names: $names")
+ gtfsRepo.gtfsDao.getRoutesFromShortNames(names)
+ }
val palinaToShow = MediatorLiveData()
val sourcesLiveData = MediatorLiveData()
@@ -81,6 +91,41 @@
}
}
+ private fun updatePalinaUserName(
+ palina: Palina?,
+ favorites: List
+ ): Boolean{
+ if (palina == null) return false
+ var modified = false
+ favorites.firstOrNull()
+ ?.stopUserName
+ ?.let { palina.stopUserName = it
+ modified = true}
+
+ return modified
+ }
+ private fun updateRoutesGtfs(palina: Palina, routes: List) : Boolean{
+ val others = mutableSetOf()
+ others.addAll(routes)
+ var updated = false
+ for (r in palina.queryAllRoutes()){
+ if(r.gtfsId != null) continue
+
+ var rRem : GtfsRoute? = null
+ for(r2 in others){
+ if(r.name == r2.shortName){
+ r.gtfsId = r2.gtfsId
+ rRem = r2
+ updated = true
+ break
+ }
+ }
+ if(rRem != null){
+ others.remove(rRem)
+ }
+ }
+ return updated
+ }
init {
palinaFromArrivals.addSource(stopFromDB){
@@ -91,24 +136,41 @@
//Log.d(DEBUG_TAG, "Merged palina: $newp, num passages: ${newp?.totalNumberOfPassages}, has coords: ${newp?.hasCoords()}")
newp?.let { pal -> palinaFromArrivals.postValue(pal) }
}
+
+ /// PALINA TAKES 3 different sources
+ // Favorites
palinaToShow.addSource(stopFavoritesData){ dat ->
val current = palinaFromArrivals.value
Log.d(DEBUG_TAG, "have palina $current and favorites data: $dat")
- if(dat!=null && current!=null){
- if(dat.size>0 && dat[0].stopUserName!=null) // is in the favorites
- current.stopUserName = dat[0].stopUserName
- //set new data in palinaLiveData
+ if(current!=null) {
+ updatePalinaUserName(current, dat)
+
+ gtfsRoutesArrivals.value?.let{ updateRoutesGtfs(current, it) }
palinaToShow.value = current
}
+
}
+ // Arrivals
palinaToShow.addSource(palinaFromArrivals){ p->
- stopFavoritesData.value?.let {it ->
- if(it.isNotEmpty() && it[0].stopUserName!=null) {
- p.stopUserName = it[0].stopUserName
- }
+ stopFavoritesData.value?.let {
+ updatePalinaUserName(p,it)
+ }
+ gtfsRoutesArrivals.value?.let{
+ updateRoutesGtfs(p,it)
}
palinaToShow.value = p
}
+ //Gtfs routes from database
+ palinaToShow.addSource(gtfsRoutesArrivals){ routes->
+ palinaFromArrivals.value?.let{ palina ->
+ stopFavoritesData.value?.let {
+ updatePalinaUserName(palina,it)
+ }
+
+ updateRoutesGtfs(palina,routes)
+ palinaToShow.value = palina
+ }
+ }
}
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
--- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ServiceAlertsViewModel.kt
@@ -22,6 +22,7 @@
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
@@ -31,22 +32,20 @@
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.google.transit.realtime.GtfsRealtime.Alert
-import it.reyboz.bustorino.backend.NetworkVolleyManager
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.data.GtfsAlertDBDownloadWorker
import it.reyboz.bustorino.data.GtfsRepository
-import it.reyboz.bustorino.data.gtfs.GtfsDatabase
+import it.reyboz.bustorino.data.gtfs.AlertWithDetails
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlin.collections.filter
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)
@@ -68,11 +67,47 @@
gtfsRepo.getAlertsByRouteID(it).map{ l -> l.filter { al->al.isActive(unixTimestamp) }}
}
- val alertsByStopLiveData = stopGtfsIdToFilter.switchMap {
- if(it.gtfsID!=null) gtfsRepo.alertsDao.getAlertsForStopGtfsId(it.gtfsID!!) else MutableLiveData()
+ //intermediate pass: request stop from GTFS database
+ private val gtfsStopLiveData = stopGtfsIdToFilter.switchMap {
+ gtfsRepo.gtfsDao.getStopByStopCode(it.ID)
+ }
+ val alertsByStopLiveData = gtfsStopLiveData.switchMap { it ->
+ val unixTimestamp = (System.currentTimeMillis()/1000)
+
+ if(it != null)
+ return@switchMap gtfsRepo.alertsDao.getAlertsForStopGtfsId("gtt:${it.internalID}").map{
+ l->l.filter { al -> al.isActive(unixTimestamp) }
+ }
+ else return@switchMap MutableLiveData()
}
val allAlertsLiveData = gtfsRepo.alertsDao.getAllAlertsLiveData()
+
+ val alertsByStopLineLiveData = MediatorLiveData>()
+
+ private fun filterAlertsLine(al: AlertWithDetails, routeGtfsId: String) : Boolean{
+ var isrelevant = false
+ for(e in al.informedEntities){
+ if(e.routeId == routeGtfsId){
+ isrelevant = true
+ break
+ }
+ }
+ return isrelevant
+ }
+ init {
+ alertsByStopLineLiveData.addSource(routeToFilter) { routeGtfsId ->
+ alertsByStopLiveData.value?.let{
+ alertsByStopLineLiveData.value = it.filter{al -> filterAlertsLine(al, routeGtfsId)}
+ }
+ }
+
+ alertsByStopLineLiveData.addSource(alertsByStopLiveData) { alerts ->
+ routeToFilter.value?.let{
+ alertsByStopLineLiveData.value = alerts.filter { al -> filterAlertsLine(al, it) }
+ }
+ }
+ }
/*
private val volleyErrorListener = Response.ErrorListener { err ->
Log.e(DEBUG_TAG, "Error getting alerts: ${err.message}", err)
@@ -100,13 +135,11 @@
*/
/// WE DO NOT KNOW HOW TO GET THE GTFS STOP ID, the one given by MaTO is the same as the stop CODE
/// but we need the ID from the stops.txt table of GTT GTFS data
- /// DISABLING THIS FUNCTION
- /*fun setStopFilter(stop: Stop) {
+ fun setStopFilter(stop: Stop) {
Log.d(DEBUG_TAG, "Setting stop to filter: ${stop.ID} - ${stop.stopDisplayName}, gtfsID: ${stop.gtfsID}")
- stopGtfsIdToFilter.value = stop
+ if(stopGtfsIdToFilter.value?.ID != stop.ID)
+ stopGtfsIdToFilter.value = stop
}
-
- */
fun setGtfsLineFilter(routeId: String) {
routeToFilter.value = routeId
}
diff --git a/app/src/main/res/layout/entry_alert_line_adapter.xml b/app/src/main/res/layout/entry_alert_line_adapter.xml
--- a/app/src/main/res/layout/entry_alert_line_adapter.xml
+++ b/app/src/main/res/layout/entry_alert_line_adapter.xml
@@ -1,23 +1,36 @@
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/entry_bus_line_passage.xml b/app/src/main/res/layout/entry_bus_line_passage.xml
--- a/app/src/main/res/layout/entry_bus_line_passage.xml
+++ b/app/src/main/res/layout/entry_bus_line_passage.xml
@@ -78,7 +78,18 @@
android:drawablePadding="0dp"
android:singleLine="true">
-
+
-
+ android:layout_below="@id/routeDestination"
+ android:layout_toStartOf="@id/bubbleImageView"
+ />
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_arrivals.xml b/app/src/main/res/layout/fragment_arrivals.xml
--- a/app/src/main/res/layout/fragment_arrivals.xml
+++ b/app/src/main/res/layout/fragment_arrivals.xml
@@ -161,7 +161,7 @@
android:textSize="19sp"
android:visibility="gone" />
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_arrivals_line_item.xml b/app/src/main/res/menu/menu_arrivals_line_item.xml
--- a/app/src/main/res/menu/menu_arrivals_line_item.xml
+++ b/app/src/main/res/menu/menu_arrivals_line_item.xml
@@ -6,4 +6,7 @@
+
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
@@ -250,6 +250,8 @@
Italiano
Inglese
Avvisi per la linea %1$s:
+ Avvisi per la fermata %1$s:
+
Controllo degli avvisi disponibili in corso
Servizio GPS non trovato sul dispositivo!
Download dati avvisi in tempo reale
@@ -264,4 +266,17 @@
Segui il sistema
Imposta tema scuro o chiaro
+
+ Avvisi di servizio
+ Impostazioni avanzate
+ Imposta URL per i dati GTFS
+ Imposta qui sotto l\'URL di base usato per scaricare i dati GTFS.
+
+ATTENZIONE!!! Se modifichi questa impostazione, l\'app potrebbe smettere di funzionare correttamente!!
+Leggi attentamente la documentazione prima di apportare modifiche.
+
+Se non sai cosa stai facendo, lascia questo campo vuoto per utilizzare l\'URL di base predefinito per i dati GTFS.
+]]>
+
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
@@ -415,5 +415,18 @@
English
Checking new alerts now
Press back again to close the app
+ Service alerts
+ Show service alerts
+ Set URL for the GTFS data
+ Set the base URL for the source of GTFS tables below.
+
+ WARNING!!! If you change this setting, the app might stop working properly!!
+ Read the documentation carefully before modifying this.
+
+ If you don\'t know what you\'re doing, leave this field blank to use the default base URL for GTFS data.
+ ]]>
+
+ Advanced settings
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -2,7 +2,9 @@
-
+
+ android:title="@string/pref_recents_group_title"
+ app:layout="@layout/title_preferences_custom"
+ >
-
+
-->
-
+
-
+
+ android:title="@string/pref_advanced_title"
+ app:layout="@layout/title_preferences_custom"
+ >
+
+
+
+
+