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" + > + + + + +