diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index ab2955f..43ef520 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,168 +1,169 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome + Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:

Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!

Come Funziona?

Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".

Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori!

Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.

Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Canale unico delle notifiche Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola
diff --git a/res/values/strings.xml b/res/values/strings.xml index 3ce997e..66b0751 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,192 +1,193 @@ BusTO Libre BusTO BusTO dev You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name + No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Lines: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.

Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!

How does it work?

This app will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".

Who worked on BusTO:
- Fabio Mazza current senior rockstar developer.
- Andrea Ugo current junior rockstar developer.
- Silviu Chiriac designer of the 2021 logo.
- Marco M rockstar tester and bug hunter.
- Ludovico Pavesi previous senior rockstar developer asd.
- Valerio Bozzolan maintainer and infrastructure sponsor.
- Marco Gagino contributor and icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

The app and the related source code are released by Valerio Bozzolan under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.

Notes

This app has been developed hoping to be useful to everyone but without ANY warranty.

This translation is kindly provided by Riccardo Caniato and Marco Gagino.

Get involved! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recents is invalid Invalid value, put a valid number Finding the position… No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to position to show it on the map Please enable GPS Database update in progress… is arriving at at the stop %1$s - %2$s Show arrivals Show stops Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Changing arrival times source… Long press to change the source of arrivals Default Default channel for notifications Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST
diff --git a/src/it/reyboz/bustorino/adapters/AdapterListener.java b/src/it/reyboz/bustorino/adapters/AdapterListener.java index 389d33c..7f61e25 100644 --- a/src/it/reyboz/bustorino/adapters/AdapterListener.java +++ b/src/it/reyboz/bustorino/adapters/AdapterListener.java @@ -1,7 +1,24 @@ +/* + BusTO - Adapter 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.adapters; import it.reyboz.bustorino.backend.Stop; public interface AdapterListener { void onTappedStop(Stop stop); } diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java index df56245..6d35384 100644 --- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -1,211 +1,230 @@ /* 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 androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import android.content.SharedPreferences; import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; - import java.util.List; +import java.util.List; import java.util.Locale; -import it.reyboz.bustorino.BuildConfig; 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.RouteSorterByArrivalTime; /** * 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 */ public class PalinaAdapter extends ArrayAdapter implements SharedPreferences.OnSharedPreferenceChangeListener { private LayoutInflater li; private static 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.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; private final String KEY_CAPITALIZE; - private Capitalize capit = Capitalize.DO_NOTHING; + private Capitalize capit; //private static final int cityIcon = R.drawable.city; // hey look, a pattern! private static class ViewHolder { TextView rowStopIcon; TextView rowRouteDestination; TextView rowRouteTimetable; } 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; } public PalinaAdapter(Context context, Palina p) { super(context, row_layout, p.queryAllRoutes()); li = LayoutInflater.from(context); sort(new RouteSorterByArrivalTime()); + /* + sort(new Comparator() { + + @Override + public int compare(Route route, Route t1) { + LinesNameSorter sorter = new LinesNameSorter(); + if(route.getNameForDisplay()!= null){ + if(t1.getNameForDisplay()!=null){ + return sorter.compare(route.getNameForDisplay(), t1.getNameForDisplay()); + } + else return -1; + } else if(t1.getNameForDisplay()!=null){ + return +1; + } + else return 0; + } + }); + + */ KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE); } /** * Some parts taken from the AdapterBusLines class.
* Some parts inspired by these enlightening tutorials:
* http://www.simplesoft.it/android/guida-agli-adapter-e-le-listview-in-android.html
* https://www.codeofaninja.com/2013/09/android-viewholder-pattern-example.html
* And some other bits and bobs TIRATI FUORI DAL NULLA CON L'INTUIZIONE INTELLETTUALE PERCHÉ * SEMBRA CHE NESSUNO ABBIA LA MINIMA IDEA DI COME FUNZIONA UN ADAPTER SU ANDROID. */ @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { ViewHolder vh; if(convertView == null) { // INFLATE! // setting a parent here is not supported and causes a fatal exception, apparently. convertView = li.inflate(row_layout, null); // STORE TEXTVIEWS! vh = new ViewHolder(); vh.rowStopIcon = (TextView) convertView.findViewById(R.id.routeID); vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination); vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere); // STORE VIEWHOLDER IN\ON\OVER\UNDER\ABOVE\BESIDE THE VIEW! convertView.setTag(vh); } else { // RECOVER THIS STUFF! vh = (ViewHolder) convertView.getTag(); } Route route = getItem(position); vh.rowStopIcon.setText(route.getNameForDisplay()); 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); } switch (route.type) { //UNKNOWN = BUS for the moment case UNKNOWN: case BUS: default: // convertView could contain another background, reset it vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: vh.rowStopIcon.setBackgroundResource(extraurbanoBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); vh.rowStopIcon.setBackgroundResource(metroBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case RAILWAY: vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: // never used but whatever. vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; } List passaggi = route.passaggi; if(passaggi.size() == 0) { vh.rowRouteTimetable.setText(R.string.no_passages); } else { vh.rowRouteTimetable.setText(route.getPassaggiToString()); } return convertView; } @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 } } diff --git a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java index 57f35fa..e184500 100644 --- a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java @@ -1,157 +1,174 @@ +/* + BusTO - Adapter 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.adapters; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; public class StopRecyclerAdapter extends RecyclerView.Adapter { private List stops; private static final int row_layout = R.layout.entry_bus_stop; private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; private static final int cityIcon = R.drawable.city; private AdapterListener listener; private int position; protected static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener{ TextView busStopIDTextView; TextView busStopNameTextView; //TextView busLineVehicleIcon; TextView busStopLinesTextView; TextView busStopLocaLityTextView; Stop mStop; public ViewHolder(@NonNull View itemView, AdapterListener listener) { super(itemView); busStopIDTextView = (TextView) itemView.findViewById(R.id.busStopID); busStopNameTextView = (TextView) itemView.findViewById(R.id.busStopName); busStopLinesTextView = (TextView) itemView.findViewById(R.id.routesThatStopHere); busStopLocaLityTextView = (TextView) itemView.findViewById(R.id.busStopLocality); mStop = new Stop(""); itemView.setOnClickListener(view -> { listener.onTappedStop(mStop); }); } //many thanks to https://stackoverflow.com/questions/26466877/how-to-create-context-menu-for-recyclerview @Override public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { MenuInflater inflater = new MenuInflater(view.getContext()); inflater.inflate(R.menu.menu_favourites_entry, contextMenu); } } public StopRecyclerAdapter(List stops,AdapterListener listener) { this.stops = stops; this.listener = listener; } public void setStops(List stops){ this.stops = stops; notifyDataSetChanged(); } public List getStops() { return stops; } public int getPosition() { return position; } public void setPosition(int position) { this.position = position; } @NonNull @Override public StopRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(row_layout, parent, false); return new StopRecyclerAdapter.ViewHolder(view, listener); } @Override public void onViewRecycled(@NonNull StopRecyclerAdapter.ViewHolder holder) { holder.itemView.setOnLongClickListener(null); super.onViewRecycled(holder); } @Override public void onBindViewHolder(@NonNull StopRecyclerAdapter.ViewHolder vh, int position) { Log.d("StopRecyclerAdapter", "Called for position "+position); Stop stop = stops.get(position); vh.busStopIDTextView.setText(stop.ID); vh.mStop = stop; Log.d("StopRecyclerAdapter", "Stop: "+stop.ID); // NOTE: intentionally ignoring stop username in search results: if it's in the favorites, why are you searching for it? vh.busStopNameTextView.setText(stop.getStopDisplayName()); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { vh.busStopLinesTextView.setVisibility(View.GONE); } else { vh.busStopLinesTextView.setText(whatStopsHere); vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } if(stop.type == null) { vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); } else { switch(stop.type) { case BUS: default: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: case RAILWAY: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: // è l'opposto della città ma va beh, dettagli. vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0); } } if (stop.location == null) { vh.busStopLocaLityTextView.setVisibility(View.GONE); } else { vh.busStopLocaLityTextView.setText(stop.location); vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } //trick to set the position vh.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { setPosition(vh.getAdapterPosition()); return false; } }); } @Override public int getItemCount() { return stops.size(); } } diff --git a/src/it/reyboz/bustorino/backend/Fetcher.java b/src/it/reyboz/bustorino/backend/Fetcher.java index 8ed79ca..f9e6bb4 100644 --- a/src/it/reyboz/bustorino/backend/Fetcher.java +++ b/src/it/reyboz/bustorino/backend/Fetcher.java @@ -1,37 +1,38 @@ /* 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.backend; public interface Fetcher { /** * Status codes.
*
* OK: got a response, parsed correctly, obtained some data
* CLIENT_OFFLINE: can't connect to the internet
* SERVER_ERROR: the server replied anything other than HTTP 200, basically
* for 404 special constant (see @FiveTAPIFetcher) * PARSER_ERROR: the server replied something that can't be parsed, probably it's not the data we're looking for (e.g. "PHP: Fatal Error")
* EMPTY_RESULT_SET: the response is valid and indicates there are no stops\routes\"passaggi"\results for your query
+ * NOT_FOUND: response is valid, no parsing errors, but the desired stops/routes wasn't found * QUERY_TOO_SHORT: input more characters and retry. */ enum Result { OK, CLIENT_OFFLINE, SERVER_ERROR, SETUP_ERROR,PARSER_ERROR, EMPTY_RESULT_SET, QUERY_TOO_SHORT, SERVER_ERROR_404, - CONNECTION_ERROR + CONNECTION_ERROR, NOT_FOUND } } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java index 093629c..530fd1f 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,430 +1,429 @@ /* BusTO - Backend components Copyright (C) 2018 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.backend; import androidx.annotation.Nullable; import android.util.Log; import it.reyboz.bustorino.data.GTTInfoInject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class FiveTAPIFetcher implements ArrivalsFetcher{ private static final String DEBUG_NAME = "FiveTAPIFetcher"; private final Map defaultHeaders = getDefaultHeaders(); final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab")); @Override public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { //set the date for the request as now Palina p = new Palina(stopID); //request parameters String response = performAPIRequest(QueryType.ARRIVALS,stopID,res); if(response==null) { if(res.get()== Result.SERVER_ERROR_404) { Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the address is wrong"); - res.set(Result.EMPTY_RESULT_SET); + //res.set(Result.S); } return p; } List routes = parseArrivalsServerResponse(response, res); if(res.get()==Result.OK) { for (Route r : routes) { p.addRoute(r); } p.sortRoutes(); } return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.FiveTAPI; } List parseArrivalsServerResponse(String JSONresponse, AtomicReference res){ ArrayList routes = new ArrayList<>(3); /* Slight problem: "longName": ==> DESCRIPTION "name": "13N", "departures": [ { "arrivalTimeInt": 1272, "time": "21:12", "rt": false }] "lineType": "URBANO" ==> URBANO can be either bus or tram or METRO */ JSONArray arr; try{ arr = new JSONArray(JSONresponse); String type; Route.Type routetype = Route.Type.UNKNOWN; for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{ if(response == null || response.equals("null") || response.length()==0) throw new IllegalArgumentException("Response string is null or void"); ArrayList routes = new ArrayList<>(10); JSONArray lines =new JSONArray(response); for(int i=0; i 1) { String secondo = exploded[exploded.length-2]; if (secondo.contains("festivo")) { festivo = Route.FestiveInfo.FESTIVO; } else if (secondo.contains("feriale")) { festivo = Route.FestiveInfo.FERIALE; } else if(secondo.contains("lun. - ven")) { serviceDays = Route.reduced_week; } else if(secondo.contains("sab - fest.")){ serviceDays = Route.weekend; festivo = Route.FestiveInfo.FESTIVO; } else { /* Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+ "Couldn't find a the service days\n"+ "Description: "+secondo+","+description ); */ } if(exploded.length>2){ switch (exploded[exploded.length-3].trim()) { case "bus": t = Route.Type.BUS; break; case "tram": //never happened, but if it could happen you can get it t = Route.Type.TRAM; break; default: //nothing } } } else //only one piece if(description.contains("festivo")){ festivo = Route.FestiveInfo.FESTIVO; } else if(description.contains("feriale")){ festivo = Route.FestiveInfo.FERIALE; } if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM; //check for the presence of parenthesis String preParenthesis, postParenthesis; boolean hasParenth = false; if (description.contains("(")){ hasParenth =true; preParenthesis = description.split("\\(")[0]; postParenthesis = description.split("\\(")[1]; } else { preParenthesis = description; postParenthesis = ""; } if(preParenthesis.contains("-")){ //Sometimes the actual filtered direction still remains the full line (including both extremes) preParenthesis = preParenthesis.split("-")[1]; } final String directionFinal = hasParenth? preParenthesis.trim() + " (" + postParenthesis : preParenthesis; Route r = new Route(lineName.trim(),directionFinal.trim(),t,new ArrayList<>()); if(serviceDays.length>0) r.serviceDays = serviceDays; r.festivo = festivo; r.branchid = branchid; r.description = description.trim(); //check if we have the stop list if (branchJSON.has("branchDetail")) { final String stops = branchJSON.getJSONObject("branchDetail").getString("stops"); r.setStopsList(Arrays.asList(stops.split(","))); } routes.add(r); } return routes; } public List getDirectionsForStop(String stopID, AtomicReference res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); List routes; try{ routes = parseDirectionsFromResponse(response); res.set(Result.OK); } catch (JSONException | IllegalArgumentException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); routes = null; } return routes; } public ArrayList getAllStopsFromGTT(AtomicReference res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); if(response==null) return null; ArrayList stopslist; try{ //JSONObject responseJSON = new JSONObject(response); JSONArray stops = new JSONArray(response);//responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i getAllLinesFromGTT(AtomicReference res){ String resp = performAPIRequest(QueryType.LINES,null,res); if(resp==null) { return null; } ArrayList routes = null; try { JSONArray lines = new JSONArray(resp); routes = new ArrayList<>(lines.length()); for(int i = 0; i getDefaultHeaders(){ HashMap param = new HashMap<>(); param.put("Host","www.5t.torino.it"); param.put("Connection","Keep-Alive"); param.put("Accept-Encoding", "gzip"); return param; } /** * Create and perform the network request. This method adds parameters and returns the result * @param t type of request to be performed * @param stopID optional parameter, stop ID which you need for passages and branches * @param res result container * @return a String which contains the result of the query, to be parsed */ @Nullable public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference res){ URL u; Map param; try { String address = getURLForOperation(t,stopID); //Log.d(DEBUG_NAME,"The address to query is: "+address); param = getDefaultHeaders(); u = new URL(address); } catch (UnsupportedEncodingException |MalformedURLException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return null; } - String response = networkTools.queryURL(u,res,param); - return response; + return networkTools.queryURL(u,res,param); } /** * Get the right url for the operation you are doing, to be fed into the queryURL method * @param t type of operation * @param stopID stop on which you are working on * @return the Url to go to * @throws UnsupportedEncodingException if it cannot be converted to utf-8 */ public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { final StringBuilder sb = new StringBuilder(); sb.append("http://www.5t.torino.it/ws2.1/rest/"); if(t!=QueryType.LINES) sb.append("stops/"); switch (t){ case ARRIVALS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/departures"); break; case DETAILS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/branches/details"); break; case STOPS_ALL: sb.append("all"); break; case STOPS_VERSION: sb.append("version"); break; case LINES: sb.append("lines/all"); break; } return sb.toString(); } public enum QueryType { ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java index 4d482e3..eb48542 100644 --- a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java @@ -1,211 +1,211 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan 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.backend; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.net.URL; import java.net.URLEncoder; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; //import android.util.Log; /** * Contains large chunks of code taken from the old GTTSiteSucker, AsyncWget and AsyncWgetBusStopFromBusStopID classes.
*
* «BusTO, because sucking happens»
*
* @author Valerio Bozzolan */ public class FiveTScraperFetcher implements ArrivalsFetcher { /** * Execute regexes. * * @param needle Regex * @param haystack Entire string * @return Matched string */ private static String grep(String needle, String haystack) { String matched = null; Matcher matcher = Pattern.compile( needle).matcher(haystack); if (matcher.find()) { matched = matcher.group(1); } return matched; } @Override public Palina ReadArrivalTimesAll(final String stopID, final AtomicReference res) { Palina p = new Palina(stopID); int routeIndex; String responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = null; try { responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = networkTools.getDOM(new URL("http://www.5t.torino.it/5t/trasporto/arrival-times-byline.jsp?action=getTransitsByLine&shortName=" + URLEncoder.encode(stopID, "utf-8")), res); } catch (Exception e) { res.set(Result.PARSER_ERROR); } if(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas == null) { // result already set in getDOM() return p; } Document doc = Jsoup.parse(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas); // Tried in rete Edisu (it does Man In The Middle... asd) Element span = doc.select("span").first(); if(span == null) { res.set(Result.SERVER_ERROR); return p; } String busStopID = grep("^(.+) ", span.html()); if (busStopID == null) { //Log.e("BusStop", "Empty busStopID from " + span.html()); - res.set(Result.EMPTY_RESULT_SET); + res.set(Result.NOT_FOUND); return p; } // this also appears when no stops are found, but that case has already been handled above Element error = doc.select("p.errore").first(); if (error != null) { res.set(Result.SERVER_ERROR); return p; } String busStopName = grep("^.+ (.+)", span.html()); // The first "dot" is the single strange space character in the middle of "39{HERE→} {←HERE}PORTA NUOVA" if (busStopName == null) { //Log.e("BusStop", "Empty busStopName from " + span.html()); res.set(Result.SERVER_ERROR); return p; } p.setStopName(busStopName.trim()); // Every table row is a busLine Elements trs = doc.select("table tr"); for (Element tr : trs) { Element line = tr.select("td.line a").first(); if (!line.hasText()) { res.set(Result.SERVER_ERROR); return p; } String busLineName = line.text(); // this is yet another ID, that has no known use so we can safely ignore it // Integer busLineID = string2Integer( // grep( // "([0-9]+)$", // line.attr("href") // ) // ); if (busLineName == null) { res.set(Result.SERVER_ERROR); return p; } // this fetcher doesn't support railways and probably they've removed METRO too, but anyway... if(busLineName.equals("METRO")) { routeIndex = p.addRoute(busLineName, "", Route.Type.METRO); } else { if(busLineName.length() >= 4) { boolean isExtraurbano = true; for(int ch = 0; ch < busLineName.length(); ch++) { if(!Character.isDigit(busLineName.charAt(ch))) { isExtraurbano = false; break; } } if(isExtraurbano) { routeIndex = p.addRoute(busLineName, "", Route.Type.LONG_DISTANCE_BUS); } else { routeIndex = p.addRoute(busLineName, "", Route.Type.BUS); } } else { routeIndex = p.addRoute(busLineName, "", Route.Type.BUS); } } // Every busLine have passages Elements tds = tr.select("td:not(.line)"); for (Element td : tds) { //boolean isInRealTime = td.select("i").size() > 0; //td.select("i").remove(); // Stripping "*" String time = td.text().trim(); if (time.equals("")) { // Yes... Sometimes there is an EMPTY td ._. continue; } p.addPassaggio(time, Passaggio.Source.FiveTScraper, routeIndex); } } p.sortRoutes(); res.set(Result.OK); return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.FiveTScraper; } // preserved for future generations: // /* // * I've sent many emails to the public email info@5t.torino.it to write down something like: // * «YOUR SITE EXPLODE IF I USE **YOUR** BUS LINE IDs STARTING WITH ZERO!!!!!» // * So, waiting for a response, I must purge the busStopID from "0"s .__. // * IN YOUR FACE 5T/GTT. IN YOUR FACE. // * // * @param busStopID // * @return parseInt(busStopID) // * @antifeatured yep // * @notabug yep // * @wontfix yep // */ // protected final String getFilteredBusStopID(String busStopID) { // /* // * OK leds me ezplain why 'm dong this shot of shittt. OK zo swhy? // * Bhumm thads because the GTT/5T site-"developer" ids obviusli drunk. // */ // String enableGTTDeveloperSimulator = "on"; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // final char ZZZZZZZEEEEROOOOOO = '0'; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // char[] cinquettiBarraGtt = busStopID.toCharArray(); // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // int merda = 0; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // while (merda < cinquettiBarraGtt.length && cinquettiBarraGtt[merda] == ZZZZZZZEEEEROOOOOO) { // // COMPLETELELELLELEEELY DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // Log.i("AsyncWgetBusStop", "scimmie ubriache assunte per tirar su il sito 5T/GTT"); // DR // merda++; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // } // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // String trenoDiMerda = ""; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // for (; merda < cinquettiBarraGtt.length; merda++) { // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // trenoDiMerda += cinquettiBarraGtt[merda]; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // } // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // enableGTTDeveloperSimulator = "off"; // DRUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUNK // // return trenoDiMerda; // } } diff --git a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java index 112eca6..2225dde 100644 --- a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java @@ -1,132 +1,132 @@ /* 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.backend; import android.util.Log; import androidx.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.concurrent.atomic.AtomicReference; public class GTTJSONFetcher implements ArrivalsFetcher { private final String DEBUG_TAG = "GTTJSONFetcher-BusTO"; @Override @NonNull public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { URL url; Palina p = new Palina(stopID); String routename; String bacino; String content; JSONArray json; int howManyRoutes, howManyPassaggi, i, j, pos; // il misto inglese-italiano è un po' ridicolo ma tanto vale... JSONObject thisroute; JSONArray passaggi; try { url = new URL("https://www.gtt.to.it/cms/index.php?option=com_gtt&task=palina.getTransitiOld&palina=" + URLEncoder.encode(stopID, "utf-8") + "&bacino=U&realtime=true&get_param=value"); } catch (Exception e) { res.set(Result.PARSER_ERROR); return p; } HashMap headers = new HashMap<>(); //headers.put("Referer","https://www.gtt.to.it/cms/percorari/urbano?view=percorsi&bacino=U&linea=15&Regol=GE"); headers.put("Host", "www.gtt.to.it"); content = networkTools.queryURL(url, res, headers); if(content == null) { Log.w("GTTJSONFetcher", "NULL CONTENT"); return p; } try { json = new JSONArray(content); } catch(JSONException e) { Log.w(DEBUG_TAG, "Error parsing JSON: \n"+content); Log.w(DEBUG_TAG, e); res.set(Result.PARSER_ERROR); return p; } try { // returns [{"PassaggiRT":[],"Passaggi":[]}] for non existing stops! json.getJSONObject(0).getString("Linea"); // if we can get this, then there's something useful in the array. } catch(JSONException e) { Log.w(DEBUG_TAG, "No existing lines"); - res.set(Result.EMPTY_RESULT_SET); + res.set(Result.NOT_FOUND); return p; } howManyRoutes = json.length(); if(howManyRoutes == 0) { res.set(Result.EMPTY_RESULT_SET); return p; } try { for(i = 0; i < howManyRoutes; i++) { thisroute = json.getJSONObject(i); routename = thisroute.getString("Linea"); try { bacino = thisroute.getString("Bacino"); } catch (JSONException ignored) { // if "Bacino" gets removed... bacino = "U"; } pos = p.addRoute(routename, thisroute.getString("Direzione"), FiveTNormalizer.decodeType(routename, bacino)); passaggi = thisroute.getJSONArray("PassaggiRT"); howManyPassaggi = passaggi.length(); for(j = 0; j < howManyPassaggi; j++) { String mPassaggio = passaggi.getString(j); if (mPassaggio.contains("__")){ mPassaggio = mPassaggio.replace("_", ""); } p.addPassaggio(mPassaggio.concat("*"), Passaggio.Source.GTTJSON, pos); } passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones howManyPassaggi = passaggi.length(); for(j = 0; j < howManyPassaggi; j++) { p.addPassaggio(passaggi.getString(j), Passaggio.Source.GTTJSON, pos); } } } catch (JSONException e) { res.set(Result.PARSER_ERROR); return p; } p.sortRoutes(); res.set(Result.OK); return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.GTTJSON; } } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 1906619..d7517dd 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,408 +1,372 @@ /* 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.backend; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import it.reyboz.bustorino.util.LinesNameSorter; + /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude()); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Double lat, @Nullable Double lon) { super(ID, name, userName, location, null, null, lat, lon); } public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { super(name, ID, location, type, routesThatStopHere); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { - this.routes.add(new Route(routeID, destinazione, type, new ArrayList<>(6))); - routesModified = true; - return this.routes.size() - 1; // last inserted element and pray that direct access to ArrayList elements really is direct + return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; - return this.routes.size()-1; + buildRoutesString(); + return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } + public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } + @Nullable + @Override + protected String buildRoutesString() { + // no routes => no string + if(routes == null || routes.size() == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + final LinesNameSorter nameSorter = new LinesNameSorter(); + Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); + int i, lenMinusOne = routes.size() - 1; + + for (i = 0; i < lenMinusOne; i++) { + sb.append(routes.get(i).getName().trim()).append(", "); + } + // last one: + sb.append(routes.get(i).getName()); + + setRoutesThatStopHereString(sb.toString()); + return routesThatStopHereToString(); + } -// /** -// * Clears a route timetable (or creates an empty route) and returns its index -// * -// * @param routeID name -// * @param destinazione end of line\terminus -// * @return array index for this route -// */ -// public int updateRoute(String routeID, String destinazione) { -// int s = this.routes.size(); -// RouteInternal r; -// -// for(int i = 0; i < s; i++) { -// r = routes.get(i); -// if(r.name.compareTo(routeID) == 0 && r.destinazione.compareTo(destinazione) == 0) { -// // capire se è possibile che ci siano stessa linea e stessa destinazione su 2 righe diverse del sito e qui una sovrascrive l'altra (probabilmente no) -// r.updateFlag(); -// r.deletePassaggio(); -// return i; -// } -// } -// -// return this.addRoute(routeID, destinazione); -// } -// -// /** -// * Deletes routes marked as "not updated" (= disappeared from the GTT website\API\whatever). -// * Sets all remaining routes to "not updated" because that's how this contraption works. -// */ -// public void finishUpdatingRoutes() { -// RouteInternal r; -// -// for(Iterator itr = this.routes.iterator(); itr.hasNext(); ) { -// r = itr.next(); -// if(r.unupdateFlag()) { -// itr.remove(); -// } -// } -// } -// /** -// * Gets the current timetable for a route. Returns null if the route doesn't exist. -// * This is slower than queryRouteByIndex. -// * -// * @return timetable (passaggi) -// */ -// public List queryRoute(String routeID) { -// for(Route r : this.routes) { -// if(routeID.equals(r.name)) { -// return r.getPassaggi(); -// } -// } -// -// return null; -// } -// -// /** -// * Gets the current timetable for this route, from its index in the array. -// * -// * @return timetable (passaggi) -// */ -// public List queryRouteByIndex(int index) { -// return this.routes.get(index).getPassaggi(); -// } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); + buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } + if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i. */ package it.reyboz.bustorino.backend; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.reyboz.bustorino.util.LinesNameSorter; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; public @Nullable Route.Type type; private @Nullable List routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; // public @Nullable String gtfsID = null; /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * Constructor that sets EVERYTHING. */ public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon) { this.ID = ID; this.name = name; this.username = userName; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildRoutesString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } + protected void setRoutesThatStopHereString(String routesStopping){ + this.routesThatStopHereString = routesStopping; + } + @Nullable protected List getRoutesThatStopHere(){ return routesThatStopHere; } - private @Nullable String buildRoutesString() { + protected @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); Collections.sort(routesThatStopHere,new LinesNameSorter()); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
* * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.
* Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } public Double getDistanceFromLocation(Location loc){ if(this.lat!=null && this.lon !=null) return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude()); else return Double.POSITIVE_INFINITY; } } diff --git a/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java b/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java index 419642f..b72ec4c 100644 --- a/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java +++ b/src/it/reyboz/bustorino/backend/gtfs/GtfsDataParser.java @@ -1,285 +1,302 @@ +/* + BusTO - Backend 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.backend.gtfs; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import de.siegmar.fastcsv.reader.CloseableIterator; import de.siegmar.fastcsv.reader.NamedCsvReader; import de.siegmar.fastcsv.reader.NamedCsvRow; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.networkTools; import it.reyboz.bustorino.data.gtfs.CsvTableInserter; import org.jsoup.Jsoup; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; abstract public class GtfsDataParser { public static final String GTFS_ADDRESS="https://www.gtt.to.it/open_data/gtt_gtfs.zip"; public static final String GTFS_PAGE_ADDRESS="http://aperto.comune.torino.it/dataset/feed-gtfs-trasporti-gtt"; private static final String DEBUG_TAG = "BusTO-GTFSDataParser"; private static final Pattern quotePattern = Pattern.compile("^\\s*\"((?:[^\"]|(?:\"\"))*?)\"\\s*,"); /** * First trial for a function to download the zip * @param res Fetcher.result * @return the list of files inside the ziè */ public static ArrayList readFilesList(AtomicReference res){ HttpURLConnection urlConnection; InputStream in; ArrayList result = new ArrayList<>(); try { final URL gtfsUrl = new URL(GTFS_ADDRESS); urlConnection = (HttpURLConnection) gtfsUrl.openConnection(); } catch(IOException e) { //e.printStackTrace(); res.set(Fetcher.Result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. return null; } urlConnection.setConnectTimeout(4000); urlConnection.setReadTimeout(50*1000); try { in = urlConnection.getInputStream(); } catch (Exception e) { try { if(urlConnection.getResponseCode()==404) res.set(Fetcher.Result.SERVER_ERROR_404); } catch (IOException e2) { e2.printStackTrace(); } return null; } try (ZipInputStream stream = new ZipInputStream(in)) { // now iterate through each item in the stream. The get next // entry call will return a ZipEntry for each file in the // stream ZipEntry entry; while ((entry = stream.getNextEntry()) != null) { String s = String.format(Locale.ENGLISH, "Entry: %s len %d added", entry.getName(), entry.getSize() ); System.out.println(s); // Once we get the entry from the stream, the stream is // positioned read to read the raw data, and we keep // reading until read returns 0 or less. result.add(entry.getName()); } } catch (IOException e) { e.printStackTrace(); } // we must always close the zip file. return result; } public static Date getLastGTFSUpdateDate(AtomicReference res) { URL theURL; final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH); //final Date baseDate = dateFormat.parse("1970-00-00T00:00:00+0000"); final Date nullDate = new Date(0); try{ theURL = new URL(GTFS_PAGE_ADDRESS); } catch (IOException ex){ Log.e(DEBUG_TAG, "Fixed URL is null, this is a real issue"); return nullDate; } res.set(Fetcher.Result.OK); final String fullPageDOM = networkTools.getDOM(theURL, res); if(fullPageDOM== null){ //Something wrong happend Log.e(DEBUG_TAG, "Cannot get URL"); return nullDate; } res.set(Fetcher.Result.OK); Document doc = Jsoup.parse(fullPageDOM); Elements sections = doc.select("section.additional-info"); Date finalDate = new Date(0); for (Element sec: sections){ Element head = sec.select("h3").first(); String headTitle = head.text(); if(!headTitle.trim().toLowerCase(Locale.ITALIAN).equals("informazioni supplementari")) continue; for (Element row: sec.select("tr")){ if(!row.selectFirst("th").text().trim() .toLowerCase(Locale.ITALIAN).equals("ultimo aggiornamento")) continue; Attributes spanAttributes = row.selectFirst("td > span").attributes(); String dateAsString = spanAttributes.get("data-datetime"); try { finalDate = dateFormat.parse(dateAsString); return finalDate; }catch (ParseException ex){ Log.e(DEBUG_TAG, "Wrong date for the last update of GTFS Data: "+dateAsString); res.set(Fetcher.Result.PARSER_ERROR); ex.printStackTrace(); } break; } } res.set(Fetcher.Result.PARSER_ERROR); return finalDate; } public static void readGtfsZipEntry(ZipEntry entry, ZipFile zipFile, Context con) throws IOException{ String tableName = entry.getName().split("\\.")[0].trim(); InputStream stream = zipFile.getInputStream(entry); String s = String.format(Locale.ENGLISH, "Entry: %s len %d added", entry.getName(), entry.getSize() ); Log.d(DEBUG_TAG, s); final BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); GtfsDataParser.readCSVWithColumns(reader, tableName, con); stream.close(); } - public static void readCSVWithColumns(BufferedReader reader, String tableName, Context con) throws IOException { + public static void readCSVWithColumns(BufferedReader reader, String tableName, Context con) { //String[] elements; List lineElements; String line; /*final String header = reader.readLine(); if (header == null){ throw new IOException(); }*/ //elements = header.split("\n")[0].split(","); //System.out.println(Arrays.toString(elements)); //lineElements = readCsvLine(header); NamedCsvReader csvReader = NamedCsvReader.builder().build(reader); CloseableIterator iterator = csvReader.iterator(); final CsvTableInserter inserter = new CsvTableInserter(tableName,con); /*final HashMap columnMap = new HashMap<>(); for (int i=0; i< lineElements.size(); i++){ //columnMap.put(i, fixStringIfItHasQuotes(elements[i].trim()) ); columnMap.put(i, lineElements.get(i).trim() ); } Log.d(DEBUG_TAG, "Columns for the file: "+columnMap); boolean first = true; while((line = reader.readLine())!=null){ //there is a line of data //elements = line.split("\n")[0].split(","); if(first) Log.d(DEBUG_TAG, "Element line: "+line); lineElements = readCsvLine(line); final Map rowsMap = getColumnsAsString(lineElements.toArray(new String[0]), columnMap); if (first){ Log.d(DEBUG_TAG, " in map:"+rowsMap); first=false; } inserter.addElement(rowsMap); }*/ int c = 0; while (iterator.hasNext()){ final Map rowsMap = iterator.next().getFields(); if (c < 1){ Log.d(DEBUG_TAG, " in map:"+rowsMap); c++; } inserter.addElement(rowsMap); } //commit data inserter.finishInsert(); } @NonNull private static Map getColumnsAsString(@NonNull String[] lineElements, Map colsIndices) { final HashMap theMap = new HashMap<>(); for(int l=0; l1){ //if(elements.length<3) throw new IllegalArgumentException("Malformed string"); return elements[1]; } else if(elements.length > 0) return elements[0]; else return item; } //https://stackoverflow.com/questions/7800494/parse-csv-with-double-quote-in-some-cases#7800519 public static List readCsvLine(String line) throws IllegalArgumentException { - List list = new ArrayList(); + List list = new ArrayList<>(); line += ","; for (int x = 0; x < line.length(); x++) { String s = line.substring(x); if (s.trim().startsWith("\"")) { Matcher m = quotePattern.matcher(s); if (!m.find()) { Log.e(DEBUG_TAG, "Cannot find pattern, "+s+" , line: "+line); throw new IllegalArgumentException("CSV is malformed"); } list.add(m.group(1).replace("\"\"", "\"")); x += m.end() - 1; } else { int y = s.indexOf(","); if (y == -1) throw new IllegalArgumentException("CSV is malformed"); list.add(s.substring(0, y)); x += y; } } return list; } } diff --git a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java index bbb7356..0ca24dd 100644 --- a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -1,109 +1,149 @@ +/* + BusTO - Backend 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.backend.mato; import android.util.Log; import androidx.annotation.Nullable; import com.android.volley.AuthFailureError; import com.android.volley.NetworkResponse; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.HttpHeaderParser; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; public class MapiArrivalRequest extends MapiVolleyRequest { private final String stopName; private final Date startingTime; private final int timeRange, numberOfDepartures; + private final AtomicReference reqRes; public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, int numberOfDepartures, + AtomicReference res, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { super(MatoAPIFetcher.QueryType.ARRIVALS, listener, errorListener); this.stopName = stopName; this.startingTime = startingTime; this.timeRange = timeRange; this.numberOfDepartures = numberOfDepartures; + this.reqRes = res; + } + public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, + int numberOfDepartures, + Response.Listener listener, + @Nullable Response.ErrorListener errorListener) { + this(stopName, startingTime, timeRange, numberOfDepartures, + new AtomicReference<>(), listener, errorListener); } @Nullable @Override public byte[] getBody() throws AuthFailureError { JSONObject variables = new JSONObject(); JSONObject data = new JSONObject(); try { data.put("operationName","AllStopsDirect"); variables.put("name", stopName); variables.put("startTime", (long) startingTime.getTime()/1000); variables.put("timeRange", timeRange); variables.put("numberOfDepartures", numberOfDepartures); data.put("variables", variables); - data.put("query", MatoAPIFetcher.QUERY_ARRIVALS); + data.put("query", MatoQueries.QUERY_ARRIVALS); } catch (JSONException e) { e.printStackTrace(); throw new AuthFailureError("Error with JSON enconding",e); } String requestBody = data.toString(); Log.d("MapiArrivalBusTO", "Request variables: "+ variables); return requestBody.getBytes(); } @Override protected Response parseNetworkResponse(NetworkResponse response) { - if(response.statusCode != 200) - return Response.error(new VolleyError("Response Error Code "+response.statusCode)); + if(response.statusCode != 200) { + reqRes.set(Fetcher.Result.SERVER_ERROR); + return Response.error(new VolleyError("Response Error Code " + response.statusCode)); + } final String stringResponse = new String(response.data); Palina p = null; try { JSONObject data = new JSONObject(stringResponse).getJSONObject("data"); JSONArray allStopsFound = data.getJSONArray("stops"); - boolean haveManyResults = allStopsFound.length() > 1; + boolean stopFound = false; for (int i=0; i getParams() throws AuthFailureError { - return new HashMap<>(); + public StopNotFoundError(String message) { + super(message); + } + + public StopNotFoundError() { + super(); + } } } diff --git a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java index 1e57b36..85451c0 100644 --- a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java @@ -1,40 +1,37 @@ package it.reyboz.bustorino.backend.mato; import androidx.annotation.Nullable; import com.android.volley.AuthFailureError; import com.android.volley.Request; import com.android.volley.Response; import java.util.Map; public abstract class MapiVolleyRequest extends Request { private static final String API_URL="https://mapi.5t.torino.it/routing/v1/routers/mat/index/graphql"; protected final Response.Listener listener; private final MatoAPIFetcher.QueryType type; public MapiVolleyRequest( MatoAPIFetcher.QueryType type, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { super(Method.POST, API_URL, errorListener); this.type = type; this.listener = listener; } - @Nullable - @Override - abstract protected Map getParams() throws AuthFailureError; - @Override protected void deliverResponse(T response) { listener.onResponse(response); } @Override public Map getHeaders() throws AuthFailureError { return MatoAPIFetcher.Companion.getREQ_PARAMETERS(); } + } diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index f808952..cc3d515 100644 --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,242 +1,261 @@ +/* + BusTO - Backend 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.backend.mato import android.content.Context import android.util.Log import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.backend.* import org.json.JSONObject import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.TimeoutException import java.util.concurrent.ExecutionException -open class MatoAPIFetcher : ArrivalsFetcher { +open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { var appContext: Context? = null set(value) { field = value!!.applicationContext } + constructor(): this(3) + override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val future = RequestFuture.newFuture() - val now = Calendar.getInstance().time; - var numMinutes = 30; + val now = Calendar.getInstance().time + var numMinutes = 30 var palina = Palina(stopID) var numPassaggi = 0 var trials = 0 - while (numPassaggi < 2 && trials < 4) { + while (numPassaggi < minNumPassaggi && trials < 4) { + numMinutes += 15 - val request = MapiArrivalRequest(stopID, now, numMinutes * 60, 10, future, future) + val request = MapiArrivalRequest(stopID, now, numMinutes * 60, 10, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue - request.setTag(VOLLEY_TAG) + request.setTag(getVolleyReqTag(QueryType.ARRIVALS)) requestQueue.add(request) - try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { palina = palinaResult - if (palina.totalNumberOfPassages > 0) { - res.set(Fetcher.Result.OK) - } else res.set(Fetcher.Result.EMPTY_RESULT_SET) numPassaggi = palina.totalNumberOfPassages - } else{ - res.set(Fetcher.Result.EMPTY_RESULT_SET) } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() + if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") + fun getVolleyReqTag(type: QueryType): String{ + return when (type){ + QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" + QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" + } + } + + /** + * Get stops from the MatoAPI, set [res] accordingly + */ + fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + val future = RequestFuture.newFuture>() + + val request = VolleyAllStopsRequest(future, future) + request.tag = getVolleyReqTag(QueryType.ALL_STOPS) + + requestQueue.add(request) + + var palinaList:List = mutableListOf() + + try { + palinaList = future.get(30, TimeUnit.SECONDS) + + res?.set(Fetcher.Result.OK) + }catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } + return palinaList + } + /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) - requestData.put("query", QUERY_ARRIVALS) + requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } + */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude ) palina.gtfsID = jsonStop.getString("gtfsId") val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() + // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.gtfsId = routeBaseInfo.getString("gtfsId").trim() baseRoutes.add(r) } - - val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") - - for (i in 0 until routesStopTimes.length()){ - val patternJSON = routesStopTimes.getJSONObject(i) - val mRoute = parseRouteStoptimesJSON(patternJSON) - - //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") - //TODO: use directionId - palina.addRoute(mRoute) - for (r in baseRoutes) { - if (palina.gtfsID != null && r.gtfsId.equals(palina.gtfsID)) { - baseRoutes.remove(r) - break + if (jsonStop.has("desc")){ + palina.location = jsonStop.getString("desc") + } + //there is also "zoneId" which is the zone of the stop (0-> city, etc) + + if(jsonStop.has("stoptimesForPatterns")) { + val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") + + for (i in 0 until routesStopTimes.length()) { + val patternJSON = routesStopTimes.getJSONObject(i) + val mRoute = parseRouteStoptimesJSON(patternJSON) + + //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") + //TODO: use directionId + palina.addRoute(mRoute) + for (r in baseRoutes) { + if (palina.gtfsID != null && r.gtfsId.equals(palina.gtfsID)) { + baseRoutes.remove(r) + break + } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() - - return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") - val routeJSON = patternJSON.getJSONObject("route"); + val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.gtfsId = gtfsId return route } - const val QUERY_ARRIVALS="""query AllStopsDirect( - ${'$'}name: String - ${'$'}startTime: Long - ${'$'}timeRange: Int - ${'$'}numberOfDepartures: Int - ) { - stops(name: ${'$'}name) { - __typename - lat - lon - gtfsId - code - name - desc - wheelchairBoarding - routes { - __typename - gtfsId - shortName - } - stoptimesForPatterns( - startTime: ${'$'}startTime - timeRange: ${'$'}timeRange - numberOfDepartures: ${'$'}numberOfDepartures - ) { - __typename - pattern { - __typename - headsign - directionId - route { - __typename - gtfsId - shortName - mode - } - } - stoptimes { - __typename - scheduledArrival - realtimeArrival - realtime - realtimeState - } - } - } - } - """ - } + fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ + val data = JSONObject() + data.put("operationName", requestName) + data.put("variables", variables) + data.put("query", query) + return data + } + + } enum class QueryType { - ARRIVALS, + ARRIVALS, ALL_STOPS } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt new file mode 100644 index 0000000..d870676 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -0,0 +1,90 @@ +/* + BusTO - Backend 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.backend.mato + +class MatoQueries { + + companion object{ + const val QUERY_ARRIVALS="""query AllStopsDirect( + ${'$'}name: String + ${'$'}startTime: Long + ${'$'}timeRange: Int + ${'$'}numberOfDepartures: Int + ) { + stops(name: ${'$'}name) { + __typename + lat + lon + gtfsId + code + name + desc + wheelchairBoarding + routes { + __typename + gtfsId + shortName + } + stoptimesForPatterns( + startTime: ${'$'}startTime + timeRange: ${'$'}timeRange + numberOfDepartures: ${'$'}numberOfDepartures + ) { + __typename + pattern { + __typename + headsign + directionId + route { + __typename + gtfsId + shortName + mode + } + } + stoptimes { + __typename + scheduledArrival + realtimeArrival + realtime + realtimeState + } + } + } + } + """ + + const val ALL_STOPS_BY_FEEDS=""" + query AllStops(${'$'}feeds: [String!]){ + stops(feeds: ${'$'}feeds) { + + lat + lon + gtfsId + code + name + desc + routes { + gtfsId + shortName + } + } + } + """ + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt new file mode 100644 index 0000000..1420152 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt @@ -0,0 +1,79 @@ +/* + BusTO - Backend 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.backend.mato + +import android.util.Log +import com.android.volley.NetworkResponse +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.HttpHeaderParser +import it.reyboz.bustorino.backend.Palina +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class VolleyAllStopsRequest( + listener: Response.Listener>, + errorListener: Response.ErrorListener, +) : MapiVolleyRequest>( + MatoAPIFetcher.QueryType.ALL_STOPS,listener, errorListener) { + private val FEEDS = JSONArray() + init { + + FEEDS.put("gtt") + } + override fun getBody(): ByteArray { + val variables = JSONObject() + variables.put("feeds", FEEDS) + + val data = MatoAPIFetcher.makeRequestParameters("AllStops", variables, MatoQueries.ALL_STOPS_BY_FEEDS) + + return data.toString().toByteArray() + } + + override fun parseNetworkResponse(response: NetworkResponse?): Response> { + if (response==null) + return Response.error(VolleyError("Null response")) + else if(response.statusCode != 200) + return Response.error(VolleyError("Response not ready, status "+response.statusCode)) + + val stringResponse = String(response.data) + val palinas = ArrayList() + + try { + val allData = JSONObject(stringResponse).getJSONObject("data") + val allStops = allData.getJSONArray("stops") + + for (i in 0 until allStops.length()){ + val jsonStop = allStops.getJSONObject(i) + palinas.add(MatoAPIFetcher.parseStopJSON(jsonStop)) + } + + } catch (e: JSONException){ + Log.e("VolleyBusTO","Cannot parse response as JSON") + e.printStackTrace() + return Response.error(VolleyError("Error parsing JSON")) + + } + return Response.success(palinas, HttpHeaderParser.parseCacheHeaders(response)) + } + companion object{ + val FEEDS_STR = arrayOf("gtt") + + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/DBUpdateWorker.java b/src/it/reyboz/bustorino/data/DBUpdateWorker.java index 9cade1b..704514a 100644 --- a/src/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/src/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,155 +1,166 @@ /* 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.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Notifications; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DBUpdateWorker extends Worker{ public static final String ERROR_CODE_KEY ="Error_Code"; public static final String ERROR_REASON_KEY = "ERROR_REASON"; public static final int ERROR_FETCHING_VERSION = 4; public static final int ERROR_DOWNLOADING_STOPS = 5; public static final int ERROR_DOWNLOADING_LINES = 6; public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; private final int notifi_ID=62341; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; + private static final long UPDATE_MIN_DELAY= 3*7*24*3600; //3 weeks + public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @SuppressLint("RestrictedApi") @NonNull @Override public Result doWork() { //register Notification channel final Context con = getApplicationContext(); Notifications.createDefaultNotificationChannel(con); final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); final int new_DB_version = DatabaseUpdate.getNewVersion(); final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); + final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); + long currentTime = System.currentTimeMillis()/1000; + final int notificationID = showNotification(); Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); + /* + SKIP CHECK (Reason: The Old API might fail at any moment) if (new_DB_version < 0){ //there has been an error final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) .putInt(ERROR_CODE_KEY,new_DB_version).build(); cancelNotification(notificationID); return ListenableWorker.Result.failure(out); } + */ //we got a good version - if (current_DB_version >= new_DB_version && !isUpdateCompulsory) { + if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) + && !isUpdateCompulsory) { //don't need to update cancelNotification(notificationID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } //start the real update AtomicReference resultAtomicReference = new AtomicReference<>(); DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); break; case ERROR_LINES_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; } cancelNotification(notificationID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); //update the version in the shared preference final SharedPreferences.Editor editor = shPr.edit(); editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); + currentTime = System.currentTimeMillis()/1000; + editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); cancelNotification(notificationID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } public static Constraints getWorkConstraints(){ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build(); } public static WorkRequest newFirstTimeWorkRequest(){ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) //.setInputData(new Data.Builder().putBoolean()) .build(); } private int showNotification(){ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), Notifications.DEFAULT_CHANNEL_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); final NotificationManagerCompat notifcManager = NotificationManagerCompat.from(getApplicationContext()); final int notification_ID = 32198; notifcManager.notify(notification_ID,builder.build()); return notification_ID; } private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.cancel(notificationID); } } diff --git a/src/it/reyboz/bustorino/data/DatabaseUpdate.java b/src/it/reyboz/bustorino/data/DatabaseUpdate.java index f82245f..f98df74 100644 --- a/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,199 +1,224 @@ /* 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.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.core.content.ContextCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; +import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; + import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { - public static final String DEBUG_TAG = "BusTO-DBUpdate"; - public static final int VERSION_UNAVAILABLE = -2; - public static final int JSON_PARSING_ERROR = -4; + public static final String DEBUG_TAG = "BusTO-DBUpdate"; + public static final int VERSION_UNAVAILABLE = -2; + public static final int JSON_PARSING_ERROR = -4; + + public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; + public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; - public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; - enum Result { + enum Result { DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } try { JSONObject resp = new JSONObject(networkRequest); return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); return JSON_PARSING_ERROR; } } /** * Run the DB Update * @param con a context * @param gres a result reference * @return result of the update */ public static Result performDBUpdate(Context con, AtomicReference gres) { final FiveTAPIFetcher f = new FiveTAPIFetcher(); + /* final ArrayList stops = f.getAllStopsFromGTT(gres); //final ArrayList cpOp = new ArrayList<>(); if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading"); return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; } - // return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database + + */ final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext()); final SQLiteDatabase db = dbHelp.getWritableDatabase(); + + final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); + if (gres.get() != Fetcher.Result.OK) { + Log.w(DEBUG_TAG, "Something went wrong downloading"); + return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + + } + //TODO: Get the type of stop from the lines //Empty the needed tables db.beginTransaction(); //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); //db.delete(LinesTable.TABLE_NAME,null,null); //put new data long startTime = System.currentTimeMillis(); - Log.d(DEBUG_TAG, "Inserting " + stops.size() + " stops"); - for (final Stop s : stops) { + Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); + for (final Palina p : palinasMatoAPI) { final ContentValues cv = new ContentValues(); - cv.put(NextGenDB.Contract.StopsTable.COL_ID, s.ID); - cv.put(NextGenDB.Contract.StopsTable.COL_NAME, s.getStopDefaultName()); - if (s.location != null) - cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, s.location); - cv.put(NextGenDB.Contract.StopsTable.COL_LAT, s.getLatitude()); - cv.put(NextGenDB.Contract.StopsTable.COL_LONG, s.getLongitude()); - if (s.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName()); - cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString()); - if (s.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, s.type.getCode()); - + cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); + cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); + if (p.location != null) + cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); + cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); + cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); + if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); + cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); + if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); + if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); } db.setTransactionSuccessful(); db.endTransaction(); long endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); final ArrayList routes = f.getAllLinesFromGTT(gres); if (routes == null) { Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); dbHelp.close(); return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; } db.beginTransaction(); startTime = System.currentTimeMillis(); for (Route r : routes) { final ContentValues cv = new ContentValues(); cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); switch (r.type) { case BUS: cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); break; case RAILWAY: cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); break; case LONG_DISTANCE_BUS: cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); break; } cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); //db.insert(LinesTable.TABLE_NAME,null,cv); int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); if (rows < 1) { //we haven't changed anything db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); } } db.setTransactionSuccessful(); db.endTransaction(); endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); dbHelp.close(); return DatabaseUpdate.Result.DONE; } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /** * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now */ public static void requestDBUpdateWithWork(Context con, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); - PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS) + PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); - if (version >= 0 && !forced) + final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10); + if ((version >= 0 || lastDBUpdateTime >=0) && !forced) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); } + /* + public static boolean isDBUpdating(){ + return false; + TODO + } + */ } diff --git a/src/it/reyboz/bustorino/data/NextGenDB.java b/src/it/reyboz/bustorino/data/NextGenDB.java index 490ed20..625f537 100644 --- a/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/src/it/reyboz/bustorino/data/NextGenDB.java @@ -1,355 +1,372 @@ /* BusTO (middleware) Copyright (C) 2018 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.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import java.util.*; import static it.reyboz.bustorino.data.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; - public static final int DATABASE_VERSION = 2; + public static final int DATABASE_VERSION = 3; public static final String DEBUG_TAG = "NextGenDB-BusTO"; //NO Singleton instance //private static volatile NextGenDB instance = null; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ + StopsTable.COL_GTFS_ID+" TEXT, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public static final String[] QUERY_COLUMN_stops_all = { - StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_LOCATION, + StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION, StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING}; public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " + StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG + " >= ? AND "+ StopsTable.COL_LONG + " <= ?"; public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?"; private final Context appContext; public NextGenDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); appContext = context.getApplicationContext(); } @Override public void onCreate(SQLiteDatabase db) { Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion<2 && newVersion == 2){ //DROP ALL TABLES db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME); db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME); db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME); db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME); //RECREATE THE TABLES WITH THE NEW SCHEMA db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); DatabaseUpdate.requestDBUpdateWithWork(appContext, true); } + if(oldVersion < 3 && newVersion == 3){ + Log.d("BusTO-Database", "Running upgrades for version 3"); + //add the new column + db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+ + " ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT "); + + // DatabaseUpdate.requestDBUpdateWithWork(appContext, true); + } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } public static String getSqlCreateStopsTable(String tableName){ return "CREATE TABLE "+tableName+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; } /** * Query some bus stops inside a map view * * You can obtain the coordinates from OSMDroid using something like this: * BoundingBoxE6 bb = mMapView.getBoundingBox(); * double latFrom = bb.getLatSouthE6() / 1E6; * double latTo = bb.getLatNorthE6() / 1E6; * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ public synchronized Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) { Stop[] stops = new Stop[0]; SQLiteDatabase db = this.getReadableDatabase(); //Cursor result=null; int count; // coordinates must be strings in the where condition String minLatRaw = String.valueOf(minLat); String maxLatRaw = String.valueOf(maxLat); String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); if(db == null) { return stops; } try { final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null); stops = getStopsFromCursorAllFields(result); result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); return stops; }finally { db.close(); } return stops; } /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} * @param result cursor from query * @return an Array of the stops found in the query */ public static Stop[] getStopsFromCursorAllFields(Cursor result){ int colID = result.getColumnIndex(StopsTable.COL_ID); int colName = result.getColumnIndex(StopsTable.COL_NAME); int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION); int colType = result.getColumnIndex(StopsTable.COL_TYPE); int colLat = result.getColumnIndex(StopsTable.COL_LAT); int colLon = result.getColumnIndex(StopsTable.COL_LONG); int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING); int count = result.getCount(); Stop[] stops = new Stop[count]; int i = 0; while(result.moveToNext()) { final String stopID = result.getString(colID).trim(); - final Route.Type type = Route.getTypeFromSymbol(result.getString(colType)); + final Route.Type type; + if(result.getString(colType) == null) type = Route.Type.BUS; + else type = Route.getTypeFromSymbol(result.getString(colType)); String lines = result.getString(colLines).trim(); String locationSometimesEmpty = result.getString(colLocation); if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) { locationSometimesEmpty = null; } stops[i++] = new Stop(stopID, result.getString(colName), null, locationSometimesEmpty, type, splitLinesString(lines), result.getDouble(colLat), result.getDouble(colLon)); } return stops; } /** * Insert batch content, already prepared as * @param content ContentValues array * @return number of lines inserted */ public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException { final SQLiteDatabase db = this.getWritableDatabase(); int success = 0; db.beginTransaction(); for (final ContentValues cv : content) { try { db.replaceOrThrow(tableName, null, cv); success++; } catch (SQLiteConstraintException d){ Log.w("NextGenDB_Insert","Failed insert with FOREIGN KEY... \n"+d.getMessage()); } catch (Exception e) { Log.w("NextGenDB_Insert", e); } } db.setTransactionSuccessful(); db.endTransaction(); return success; } + int updateLinesStoppingInStop(List stops){ + return 0; + } + public static List splitLinesString(String linesStr){ return Arrays.asList(linesStr.split("\\s*,\\s*")); } public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public interface DataTables { String getTableName(); String[] getFields(); } public static final class LinesTable implements BaseColumns, DataTables { //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "line_name"; public static final String COLUMN_DESCRIPTION = "line_description"; public static final String COLUMN_TYPE = "line_bacino"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE}; } } public static final class BranchesTable implements BaseColumns, DataTables { public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "branch_direzione"; public static final String COL_FESTIVO = "branch_festivo"; public static final String COL_TYPE = "branch_type"; public static final String COL_LUN="runs_lun"; public static final String COL_MAR="runs_mar"; public static final String COL_MER="runs_mer"; public static final String COL_GIO="runs_gio"; public static final String COL_VEN="runs_ven"; public static final String COL_SAB="runs_sab"; public static final String COL_DOM="runs_dom"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION, COL_DIRECTION,COL_FESTIVO,COL_TYPE, COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM }; } } public static final class ConnectionsTable implements DataTables { public static final String TABLE_NAME = "connections"; public static final String COLUMN_BRANCH = "branchid"; public static final String COLUMN_STOP_ID = "stopid"; public static final String COLUMN_ORDER = "ordine"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER}; } } public static final class StopsTable implements DataTables { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "stopid"; //integer public static final String COL_TYPE = "stop_type"; public static final String COL_NAME = "stop_name"; + public static final String COL_GTFS_ID = "gtfs_id"; public static final String COL_LAT = "stop_latitude"; public static final String COL_LONG = "stop_longitude"; public static final String COL_LOCATION = "stop_location"; public static final String COL_PLACE = "stop_placeName"; public static final String COL_LINES_STOPPING = "stop_lines"; + @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { - return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; + return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; } } } public static final class DBUpdatingException extends Exception{ public DBUpdatingException(String message) { super(message); } } } diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java index 6eba1d3..3628a3e 100644 --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,516 +1,516 @@ /* BusTO - Fragments components Copyright (C) 2018 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.fragments; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class ArrivalsFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks { private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment"; private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; static final String STOP_TITLE = "messageExtra"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; private boolean fetchersChangeRequestPending = false; private boolean stopIsInFavorites = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; private List fetchers = new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); } public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); //parameter for ResultListFragmentrequestArrivalsForStopID args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null){ args.putString(KEY_STOP_NAME,stopName); } fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID; //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); resultsListView = (ListView) root.findViewById(R.id.resultsListView); timesSourceTextView = (TextView) root.findViewById(R.id.timesSourceTextView); timesSourceTextView.setOnLongClickListener(view -> { if(!fetchersChangeRequestPending){ rotateFetchers(); //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing); mListener.requestArrivalsForStopID(stopID); fetchersChangeRequestPending = true; return true; } return false; }); timesSourceTextView.setOnClickListener(view -> { Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT) .show(); }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites toggleLastStopToFavorites(); }); resultsListView.setOnItemClickListener((parent, view, position, id) -> { String routeName; Route r = (Route) parent.getItemAtPosition(position); routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay()); if (routeName == null) { routeName = r.getNameForDisplay(); } if (r.destinazione == null || r.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show(); } }); String displayName = getArguments().getString(STOP_TITLE); if(displayName!=null) setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated); /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ if(stopID!=null){ //refresh the arrivals if(!justCreated){ if (reloadOnResume) mListener.requestArrivalsForStopID(stopID); } else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { Log.d(DEBUG_TAG, "Restarting loader for stop"); loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach = false; } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders()); loaderManager.destroyLoader(loaderFavId); } @Nullable public String getStopID() { return stopID; } public boolean reloadsOnResume() { return reloadOnResume; } public void setReloadOnResume(boolean reloadOnResume) { this.reloadOnResume = reloadOnResume; } /** * Give the fetchers * @return the list of the fetchers */ public ArrayList getCurrentFetchers(){ ArrayList v = new ArrayList(); for (ArrivalsFetcher fetcher: fetchers){ v.add(fetcher); } return v; } - public Fetcher[] getCurrentFetchersAsArray(){ - Fetcher[] arr = new Fetcher[fetchers.size()]; + public ArrivalsFetcher[] getCurrentFetchersAsArray(){ + ArrivalsFetcher[] arr = new ArrivalsFetcher[fetchers.size()]; fetchers.toArray(arr); return arr; } private void rotateFetchers(){ Collections.rotate(fetchers, -1); } /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ if (p!=null) lastUpdatedPalina = p; if (!isAdded()){ //defer update at next show if (p==null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null"); else needUpdateOnAttach = true; } else { final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina); showArrivalsSources(lastUpdatedPalina); super.resetListAdapter(adapter); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); if (source == null){ Log.e(DEBUG_TAG, "NULL SOURCE"); return; } String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; case MatoAPI: source_txt = getString(R.string.source_mato); break; case UNDETERMINED: //Don't show the view source_txt = ""; break; default: throw new IllegalStateException("Unexpected value: " + source); } int count = 0; if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 100){ //we need to update the fetcher that is requested rotateFetchers(); count++; } if (count>10) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work"); final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setVisibility(View.VISIBLE); timesSourceTextView.setText(base_message); fetchersChangeRequestPending = false; } @Override public void setNewListAdapter(ListAdapter adapter) { throw new UnsupportedOperationException(); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && stopName.length() > 0) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ // IT'S IN FAVORITES data.moveToFirst(); final String probableName = data.getString(colUserName); stopIsInFavorites = true; stopName = probableName; //update the message in the textview updateMessage(); } else { stopIsInFavorites =false; } updateStarIcon(); if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); stopName = data.getString(data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME )); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } public void toggleLastStopToFavorites() { Stop stop = lastUpdatedPalina; if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE, v->updateStarIconFromLastBusStop(v)).execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true); } } /** * Update the star "Add to favorite" icon */ public void updateStarIconFromLastBusStop(Boolean toggleDone) { if (stopIsInFavorites) stopIsInFavorites = !toggleDone; else stopIsInFavorites = toggleDone; updateStarIcon(); // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ public void updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index 33d2ac9..caf4cd0 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,271 +1,282 @@ /* BusTO (fragments) Copyright (C) 2018 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.fragments; import android.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; + +import android.os.AsyncTask; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; private final FragmentListenerMain listenerMain; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; - private WeakReference lastTaskRef; + private WeakReference lastTaskRef; + private SearchRequestType lastTaskType; private boolean shouldHaltAllActivities=false; public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { this.listenerMain = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } - public void setLastTaskRef(WeakReference lastTaskRef) { - this.lastTaskRef = lastTaskRef; + public void setLastTaskRef(AsyncTask task) { + this.lastTaskRef = new WeakReference<>(task); } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){ boolean sameFragment; ArrivalsFragment arrivalsFragment = null; if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } FragmentManager fm = managerWeakRef.get(); if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); //Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?"); if (arrivalsFragment == null) sameFragment = false; else sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else { sameFragment = false; Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); } setLastSuccessfullySearchedBusStop(p); if (sameFragment){ Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); if (arrivalsFragment == null) sameFragment = false; } if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } String probableTag = ResultListFragment.getFragmentTag(p); attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } // DO NOT CALL `setListAdapter` ever on arrivals fragment arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); listenerMain.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ listenerMain.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } attachFragmentToContainer(managerWeakRef.get(),listfragment, new AttachParameters("search_"+query, false,addToBackStack)); listfragment.setStopList(resultList); listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ listenerMain.toggleSpinner(on); } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param parameters attach parameters */ protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){ if(shouldHaltAllActivities) //nothing to do return; FragmentTransaction ft = fm.beginTransaction(); int frameID; if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) // ft.replace(secondaryFrameLayout,fragment,tag); frameID = secondaryFrameLayout; else frameID = primaryFrameLayout; switch (parameters.transaction){ case REPLACE: ft.replace(frameID,fragment,parameters.tag); } if (parameters.addToBackStack) ft.addToBackStack("state_"+parameters.tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); if(!fm.isDestroyed() && !shouldHaltAllActivities) ft.commit(); //fm.executePendingTransactions(); } public synchronized void setBlockAllActivities(boolean shouldI) { this.shouldHaltAllActivities = shouldI; } public void stopLastRequestIfNeeded(boolean interruptIfRunning){ if(lastTaskRef == null) return; - AsyncDataDownload task = lastTaskRef.get(); + AsyncTask task = lastTaskRef.get(); if(task!=null){ task.cancel(interruptIfRunning); } } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ - public void showErrorMessage(Fetcher.Result res){ + public void showErrorMessage(Fetcher.Result res, SearchRequestType type){ //TODO: implement a common set of errors for all fragments + Log.d(DEBUG_TAG, "Showing result for "+res); switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: + if (type == SearchRequestType.STOPS) + showShortToast(R.string.no_bus_stop_have_this_name); + else if(type == SearchRequestType.ARRIVALS){ + showShortToast(R.string.no_arrivals_stop); + } + break; + case NOT_FOUND: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } enum Transaction{ REPLACE, } static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index 22771de..d7d3a68 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,739 +1,740 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.zxing.integration.android.IntentIntegrator; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.AppLocationManager; -import it.reyboz.bustorino.middleware.AsyncDataDownload; +import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; +import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{ private static final String OPTION_SHOW_LEGEND = "show_legend"; private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private boolean setupOnAttached = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; private final ArrivalsFetcher[] arrivalsFetchers = utils.getDefaultArrivalsFetchers(); //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); - new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); + new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG - new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(); + new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown()){ //request Stops pendingNearbyStopsRequest = false; if (getContext()!= null) mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ pendingNearbyStopsRequest = false; mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @Override public void onLocationDisabled() { } }; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) ) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } } } }); //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { MainScreenFragment fragment = new MainScreenFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(getContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Setup on attached: "+setupOnAttached); //Restore instance state if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnAttached = false; } } if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! mainHandler.post(new NearbyStopsRequester(context, cr)); else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnAttached = false; } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called"); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } else if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //we have already asked for the location, and we should show an explanation in order // to ask again (TODO) //do nothing } else{ //request permission requestPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS); } } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } suppressArrivalsReload = false; } if(pendingStopID!=null){ requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 3){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); - new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query); + new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } void showNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); NearbyStopsFragment fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ //there is no fragment showing fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); FragmentTransaction ft = fragMan.beginTransaction(); //if (oldFrag != null) // ft.remove(oldFrag); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); ft.commit(); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() - new AsyncDataDownload(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); + new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ - new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); + new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { - new AsyncDataDownload(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); + new AsyncArrivalsSearcher(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java similarity index 59% rename from src/it/reyboz/bustorino/middleware/AsyncDataDownload.java rename to src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java index 12d2c97..d180c42 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java @@ -1,390 +1,331 @@ /* BusTO (middleware) Copyright (C) 2018 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.middleware; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; import android.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.Calendar; /** * This should be used to download data, but not to display it */ -public class AsyncDataDownload extends AsyncTask{ +public class AsyncArrivalsSearcher extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference res; - private final RequestType t; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); - private final Fetcher[] theFetchers; + private final ArrivalsFetcher[] theFetchers; @SuppressLint("StaticFieldLeak") private final Context context; private final boolean replaceFragment; - public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) { - RequestType type; + public AsyncArrivalsSearcher(FragmentHelper fh, @NonNull ArrivalsFetcher[] fetchers, Context context) { helperRef = new WeakReference<>(fh); - fh.setLastTaskRef(new WeakReference<>(this)); + fh.setLastTaskRef(this); res = new AtomicReference<>(); this.context = context.getApplicationContext(); this.replaceFragment = true; theFetchers = fetchers; if (theFetchers.length < 1){ throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); } - if (theFetchers[0] instanceof ArrivalsFetcher){ - type = RequestType.ARRIVALS; - } else if (theFetchers[0] instanceof StopsFinderByName){ - type = RequestType.STOPS; - } else{ - type = null; - } - t = type; } @Override - protected Object doInBackground(String... params) { - RecursionHelper r = new RecursionHelper<>(theFetchers); - boolean success=false; - Object result; + protected Palina doInBackground(String... params) { + RecursionHelper r = new RecursionHelper<>(theFetchers); + Palina result = null; FragmentHelper fh = helperRef.get(); ArrayList results = new ArrayList<>(theFetchers.length); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ return null; } //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); while(r.valid()) { if(this.isCancelled()) { return null; } //get the data from the fetcher - switch (t){ - case ARRIVALS: - ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); - if (f instanceof MatoAPIFetcher){ - ((MatoAPIFetcher)f).setAppContext(context); - } - Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); - - Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); - Palina p; - String stopID; - if(params.length>0) - stopID=params[0]; //(it's a Palina) - else if(lastSearchedBusStop!=null) - stopID = lastSearchedBusStop.ID; //(it's a Palina) - else { - publishProgress(Fetcher.Result.QUERY_TOO_SHORT); - return null; - } - //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times - try { - if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) - continue; - } catch (NumberFormatException ex){ - Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); - } - p= f.ReadArrivalTimesAll(stopID,res); - - //if (res.get()!= Fetcher.Result.OK) - Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+res.get()); - - - if(f instanceof FiveTAPIFetcher){ - AtomicReference gres = new AtomicReference<>(); - List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); - Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); - if(gres.get() == Fetcher.Result.OK){ - p.addInfoFromRoutes(branches); - Thread t = new Thread(new BranchInserter(branches, context)); - t.start(); - otherActivities.add(t); - - } - //put updated values into Database - } + ArrivalsFetcher f = r.getAndMoveForward(); + AtomicReference resRef = new AtomicReference<>(); + if (f instanceof MatoAPIFetcher){ + ((MatoAPIFetcher)f).setAppContext(context); + } + Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); + + Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); + Palina p; + String stopID; + if(params.length>0) + stopID=params[0]; //(it's a Palina) + else if(lastSearchedBusStop!=null) + stopID = lastSearchedBusStop.ID; //(it's a Palina) + else { + publishProgress(Fetcher.Result.QUERY_TOO_SHORT); + return null; + } + //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times + try { + if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) + continue; + } catch (NumberFormatException ex){ + Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); + } + p= f.ReadArrivalTimesAll(stopID,resRef); + + + //if (res.get()!= Fetcher.Result.OK) + Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+resRef.get()); + + if(f instanceof FiveTAPIFetcher){ + AtomicReference gres = new AtomicReference<>(); + List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); + Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); + if(gres.get() == Fetcher.Result.OK){ + p.addInfoFromRoutes(branches); + Thread t = new Thread(new BranchInserter(branches, context)); + t.start(); + otherActivities.add(t); + } else{ + resRef.set(Fetcher.Result.NOT_FOUND); + } + //put updated values into Database + } - if(lastSearchedBusStop != null && res.get()== Fetcher.Result.OK) { - // check that we don't have the same stop - if(lastSearchedBusStop.ID.equals(p.ID)) { - // searched and it's the same - String sn = lastSearchedBusStop.getStopDisplayName(); - if(sn != null) { - // "merge" Stop over Palina and we're good to go - p.mergeNameFrom(lastSearchedBusStop); - } - } + if(lastSearchedBusStop != null && resRef.get()== Fetcher.Result.OK) { + // check that we don't have the same stop + if(lastSearchedBusStop.ID.equals(p.ID)) { + // searched and it's the same + String sn = lastSearchedBusStop.getStopDisplayName(); + if(sn != null) { + // "merge" Stop over Palina and we're good to go + p.mergeNameFrom(lastSearchedBusStop); } - p.mergeDuplicateRoutes(0); - if (p.getTotalNumberOfPassages() == 0) - res.set(Fetcher.Result.EMPTY_RESULT_SET); - publishProgress(res.get()); - //p.sortRoutes(); - result = p; - - //TODO: find a way to avoid overloading the user with toasts - break; - case STOPS: - StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward(); - - List resultList= finder.FindByName(params[0], this.res); //it's a List - Log.d(TAG,"Using the StopFinderByName: "+finder.getClass()); - query =params[0]; - result = resultList; //dummy result - Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultList.size()+" stops"); - break; - default: - result = null; + } + } + p.mergeDuplicateRoutes(0); + if (resRef.get() == Fetcher.Result.OK && p.getTotalNumberOfPassages() == 0 ) { + resRef.set(Fetcher.Result.EMPTY_RESULT_SET); + Log.d(DEBUG_TAG, "Setting empty results"); + } + publishProgress(resRef.get()); + //TODO: find a way to avoid overloading the user with toasts + if (result == null){ + result = p; } //find if it went well - results.add(res.get()); - if(res.get()== Fetcher.Result.OK) { + results.add(resRef.get()); + if(resRef.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } - return result; + return p; } } boolean emptyResults = true; for (Fetcher.Result re: results){ if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { emptyResults = false; break; } } - if(emptyResults){ - if(t==RequestType.STOPS) - publishProgress(Fetcher.Result.EMPTY_RESULT_SET); - } //at this point, we are sure that the result has been negative failedAll=true; - return null; + return result; } @Override protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.Result r : values){ //TODO: make Toast - fh.showErrorMessage(r); + fh.showErrorMessage(r, SearchRequestType.ARRIVALS); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override - protected void onPostExecute(Object o) { + protected void onPostExecute(Palina p) { FragmentHelper fh = helperRef.get(); - if(failedAll || o == null || fh == null){ + if(failedAll || p == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; - switch (t){ - case ARRIVALS: - Palina palina = (Palina) o; - fh.createOrUpdateStopFragment(palina, replaceFragment); - break; - case STOPS: - //this should never be a problem - if(!(o instanceof List)){ - throw new IllegalStateException(); - } - List list = (List) o; - if (list.size() ==0) return; - Object firstItem = list.get(0); - if(!(firstItem instanceof Stop)) return; - ArrayList stops = new ArrayList<>(); - for(Object x: list){ - if(x instanceof Stop) stops.add((Stop) x); - //Log.d(DEBUG_TAG, "Parsing Stop: "+x); - } - if(list.size() != stops.size()){ - Log.w(DEBUG_TAG, "Wrong stop list size:\n incoming: "+ - list.size()+" out: "+stops.size()); - } - //List stopList = (List) list; - if(query!=null && !isCancelled()) { - fh.createStopListFragment(stops,query, replaceFragment); - } else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT"); - break; - case DBUPDATE: - break; - } + + fh.createOrUpdateStopFragment( p, replaceFragment); } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } - public enum RequestType { - ARRIVALS,STOPS,DBUPDATE - } - public static class BranchInserter implements Runnable{ private final List routesToInsert; private final Context context; //private final NextGenDB nextGenDB; public BranchInserter(List routesToInsert,@NonNull Context con) { this.routesToInsert = routesToInsert; this.context = con.getApplicationContext(); //nextGenDB = new NextGenDB(context); } @Override public void run() { final NextGenDB nextGenDB = new NextGenDB(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList branchesValues = new ArrayList<>(routesToInsert.size()*4); ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); //values[routesToInsert.indexOf(r)] = cv; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i0) { starttime = System.currentTimeMillis(); ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); } nextGenDB.close(); } } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java b/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java new file mode 100644 index 0000000..4f65a23 --- /dev/null +++ b/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java @@ -0,0 +1,131 @@ +/* + BusTO (middleware) + 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.middleware; + +import android.os.AsyncTask; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import it.reyboz.bustorino.backend.Fetcher; +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.StopsFinderByName; +import it.reyboz.bustorino.fragments.FragmentHelper; + +public class AsyncStopsSearcher extends AsyncTask> { + + private static final String TAG = "BusTO-StopsSearcher"; + private static final String DEBUG_TAG = TAG; + private final StopsFinderByName[] fetchers; + private final AtomicReference res; + + private WeakReference helperWR; + + private String theQuery; + + public AsyncStopsSearcher(FragmentHelper fh, StopsFinderByName[] fetchers) { + this.fetchers = fetchers; + if (fetchers.length < 1){ + throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); + } + + this.res = new AtomicReference<>(); + this.helperWR = new WeakReference<>(fh); + fh.setLastTaskRef(this); + + } + + @Override + protected List doInBackground(String... strings) { + RecursionHelper r = new RecursionHelper<>(fetchers); + if (helperWR.get()==null || strings.length == 0) + return null; + Log.d(DEBUG_TAG,"Running with query "+strings[0]); + + ArrayList results = new ArrayList<>(); + List resultsList; + while (r.valid()){ + if (this.isCancelled()) return null; + + final StopsFinderByName finder = r.getAndMoveForward(); + theQuery = strings[0].trim(); + resultsList = finder.FindByName(theQuery, res); + Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultsList.size()+" stops"); + + if (res.get()== Fetcher.Result.OK){ + return resultsList; + } + results.add(res.get()); + } + boolean emptyResults = true; + for (Fetcher.Result re: results){ + if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { + emptyResults = false; + break; + } + } + if(emptyResults){ + publishProgress(Fetcher.Result.EMPTY_RESULT_SET); + } + return new ArrayList<>(); + } + + @Override + protected void onProgressUpdate(Fetcher.Result... values) { + FragmentHelper fh = helperWR.get(); + if (fh!=null) + for (Fetcher.Result r : values){ + fh.showErrorMessage(r, SearchRequestType.STOPS); + } + else { + Log.w(TAG,"We had to show some progress but activity was destroyed"); + } + } + @Override + protected void onCancelled() { + FragmentHelper fh = helperWR.get(); + if (fh!=null) fh.toggleSpinner(false); + } + + @Override + protected void onPreExecute() { + FragmentHelper fh = helperWR.get(); + if (fh!=null) fh.toggleSpinner(true); + } + + @Override + protected void onPostExecute(List stops) { + final FragmentHelper fh = helperWR.get(); + + if (stops==null || fh==null || theQuery==null) { + if (fh!=null) fh.toggleSpinner(false); + cancel(true); + return; + } + if(isCancelled()){ + fh.toggleSpinner(false); + return; + } + fh.createStopListFragment(stops, theQuery, true); + + + } +} diff --git a/src/it/reyboz/bustorino/middleware/SearchRequestType.java b/src/it/reyboz/bustorino/middleware/SearchRequestType.java new file mode 100644 index 0000000..90cf050 --- /dev/null +++ b/src/it/reyboz/bustorino/middleware/SearchRequestType.java @@ -0,0 +1,5 @@ +package it.reyboz.bustorino.middleware; + +public enum SearchRequestType { + ARRIVALS,STOPS,DBUPDATE +} diff --git a/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt b/src/it/reyboz/bustorino/util/RouteSorterByArrivalTime.kt similarity index 89% rename from src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt rename to src/it/reyboz/bustorino/util/RouteSorterByArrivalTime.kt index d6a10f1..c245d11 100644 --- a/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt +++ b/src/it/reyboz/bustorino/util/RouteSorterByArrivalTime.kt @@ -1,29 +1,29 @@ -package it.reyboz.bustorino.adapters +package it.reyboz.bustorino.util import it.reyboz.bustorino.backend.Route class RouteSorterByArrivalTime : Comparator { override fun compare(route1: Route?, route2: Route?): Int { if (route1 == null){ if(route2 == null) return 0 - else return 2; + else return 2 } else if (route2 == null){ - return -2; + return -2 } val passaggi1 = route1.passaggi val passaggi2 = route2.passaggi // handle the case of midnight if (passaggi1 == null || passaggi1.size == 0){ if (passaggi2 == null || passaggi2.size == 0) return 0 else return 2 } else if (passaggi2 == null || passaggi2.size == 0){ return -2 } passaggi1.sort() passaggi2.sort() return passaggi1[0].compareTo(passaggi2[0]) } } \ No newline at end of file