diff --git a/res/layout/fragment_arrivals.xml b/res/layout/fragment_arrivals.xml index 190516a..ce184fe 100644 --- a/res/layout/fragment_arrivals.xml +++ b/res/layout/fragment_arrivals.xml @@ -1,49 +1,98 @@ + fab:cardElevation="2dp"> + android:layout_toLeftOf="@+id/addToFavorites" + + android:foreground="?attr/selectableItemBackground" + android:gravity="center_vertical" + android:minHeight="40dp" + android:textAppearance="?android:attr/textAppearanceMedium" /> + tools:ignore="OnClick" /> - + android:orientation="vertical" + + android:layout_height="wrap_content"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 5172f0a..e52be70 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,118 +1,126 @@ 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 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 News Invia bug Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh! Schiaccia 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. 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.
- Ludovico Pavesi ex rockstar developer anziano.
- 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 di fermate Impostazioni Impostazioni Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Abilitare il GPS Raggio di ricerca Funzionalità sperimentali 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 +
diff --git a/res/values/strings.xml b/res/values/strings.xml index f78d599..525f6c2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,129 +1,136 @@ BusTO 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 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 News releases Bug submission Source code Licence 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 GOT IT! 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.
- Ludovico Pavesi previous senior rockstar developer.
- 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 Finding the position… No stops nearby Number of stops Preferences Settings Settings Experimental features Search radius Recent stops General settings Database management Launch manual database update 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
diff --git a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java index 5f39aa6..d5da78f 100644 --- a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java @@ -1,206 +1,206 @@ /* 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); 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.addPassaggio(time, Passaggio.Source.FiveTScraper, routeIndex); } } p.sortRoutes(); res.set(result.OK); return p; } // 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/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 62e0231..a67c86c 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,317 +1,349 @@ /* 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 java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; /** * 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()); } /** * 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))); + 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 } public int addRoute(Route r){ this.routes.add(r); + routesModified = true; return this.routes.size()-1; } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } // /** // * 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.UNKNOWN; + + break; + } + } + if(mSource == Passaggio.Source.UNKNOWN) + break; + } + //finished with the check, setting flags + routesModified = false; + allSource = mSource; + } + + 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); 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++; } 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; // } // } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java index d4bdf72..e09901d 100644 --- a/src/it/reyboz/bustorino/backend/Passaggio.java +++ b/src/it/reyboz/bustorino/backend/Passaggio.java @@ -1,159 +1,159 @@ /* 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.support.annotation.NonNull; import android.util.Log; public final class Passaggio implements Comparable { private static final int UNKNOWN_TIME = -3; private static final String DEBUG_TAG = "BusTO-Passaggio"; private final String passaggioGTT; public final int hh,mm; public final boolean isInRealTime; - public final Source passageSource; + public final Source source; /** * Useless constructor. * * //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. */ // public Passaggio(@NonNull String TimeGTT) { // this.passaggio = TimeGTT; // } @Override public String toString() { return this.passaggioGTT; } /** * Constructs a time (passaggio) for the timetable. * * @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. * @throws IllegalArgumentException if nothing reasonable can be extracted from the string */ public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) { passaggioGTT = TimeGTT; - passageSource = sorgente; + source = sorgente; String[] parts = TimeGTT.split(":"); String hh,mm; boolean realtime; if(parts.length != 2) { //throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); this.hh = UNKNOWN_TIME; this.mm = UNKNOWN_TIME; this.isInRealTime = false; return; } hh = parts[0]; if(parts[1].endsWith("*")) { mm = parts[1].substring(0, parts[1].length() - 1); realtime = true; } else { mm = parts[1]; realtime = false; } int hour=-3,min=-3; try { hour = Integer.parseInt(hh); min = Integer.parseInt(mm); } catch (NumberFormatException ex){ Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes"); hour = UNKNOWN_TIME; min = UNKNOWN_TIME; realtime = false; } finally { this.hh = hour; this.mm = min; this.isInRealTime = realtime; } } public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){ this.hh = hour; this.mm = minutes; this.isInRealTime = realtime; - this.passageSource = sorgente; + this.source = sorgente; //Build the passaggio string StringBuilder sb = new StringBuilder(); sb.append(hour).append(":").append(minutes); if(realtime) sb.append("*"); this.passaggioGTT = sb.toString(); } public static String createPassaggioGTT(String timeInput, boolean realtime){ final String time = timeInput.trim(); if(time.contains("*")){ if(realtime) return time; else return time.substring(0,time.length()-1); } else{ if(realtime) return time.concat("*"); else return time; } } @Override public int compareTo(@NonNull Passaggio other) { if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME) return 0; else { int diff = this.hh - other.hh; // an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01) if (diff > 12) { // untested diff -= 24; } else if (diff < -12) { diff += 24; } diff *= 60; diff += this.mm - other.mm; // we should take into account if one is in real time and the other isn't, shouldn't we? if (other.isInRealTime) { ++diff; } if (this.isInRealTime) { --diff; } //TODO: separate Realtime and Non-Realtime, especially for the GTTJSONFetcher return diff; } } // // @Override // public String toString() { // String resultString = (this.hh).concat(":").concat(this.mm); // if(this.isInRealTime) { // return resultString.concat("*"); // } else { // return resultString; // } // } public enum Source{ - FiveTAPI,GTTJSON,FiveTScraper + FiveTAPI,GTTJSON,FiveTScraper, UNKNOWN } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java index 2a6142e..3b31b66 100644 --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,285 +1,342 @@ /* 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 android.support.annotation.Nullable; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.FiveTNormalizer; 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.middleware.AppDataProvider; import it.reyboz.bustorino.middleware.NextGenDB; import it.reyboz.bustorino.middleware.UserDB; 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 = "BUSTOArrivalsFragment"; 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; //Views protected ImageButton addToFavorites; + protected TextView timesSourceTextView; public static ArrivalsFragment newInstance(String stopID){ Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); ArrivalsFragment fragment = new ArrivalsFragment(); //parameter for ResultListFragment args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); fragment.setArguments(args); return fragment; } public static ArrivalsFragment newInstance(String stopID,String stopName){ ArrivalsFragment fragment = newInstance(stopID); Bundle args = fragment.getArguments(); 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); //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); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites mListener.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); 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(); if(stopID!=null){ //refresh the arrivals if(!justCreated) mListener.createFragmentForStop(stopID); else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } } + @Override + public void onStart() { + super.onStart(); + if (needUpdateOnAttach){ + updateFragmentData(null); + } + } + @Nullable public String getStopID() { return stopID; } + /** + * 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 { - public void updateFragmentData(Palina p, PalinaAdapter adapter){ + final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina); + showArrivalsSources(lastUpdatedPalina); + super.resetListAdapter(adapter); + } + } - 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(); + + 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 UNKNOWN: + //Don't show the view + timesSourceTextView.setVisibility(View.GONE); + return; + default: + throw new IllegalStateException("Unexpected value: " + source); + } + final String base_message = getString(R.string.times_source_fmt, source_txt); + timesSourceTextView.setVisibility(View.VISIBLE); + timesSourceTextView.setText(base_message); } @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 mListener.updateStarIconFromLastBusStop(); } @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){ data.moveToFirst(); final String probableName = data.getString(colUserName); if(probableName!=null && !probableName.isEmpty()){ stopName = probableName; updateMessage(); } } 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 onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index 105fe04..5dfc492 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,232 +1,232 @@ /* 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.ContentResolver; import android.content.ContentValues; import android.database.sqlite.SQLiteException; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Stop; 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 Stop lastSuccessfullySearchedBusStop; //support for multiple frames private int primaryFrameLayout,secondaryFrameLayout, swipeRefID; public static final int NO_FRAME = -3; private WeakReference lastTaskRef; private NextGenDB newDBHelper; private boolean shouldHaltAllActivities=false; public FragmentHelper(GeneralActivity act, int swipeRefID, int mainFrame) { this(act,swipeRefID,mainFrame,NO_FRAME); } public FragmentHelper(GeneralActivity act, int swipeRefID, int primaryFrameLayout, int secondaryFrameLayout) { this.act = act; this.swipeRefID = swipeRefID; this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; newDBHelper = new NextGenDB(act.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; } /** * 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 sameFragment; ArrivalsFragment arrivalsFragment; if(act==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG return; } FragmentManager fm = act.getSupportFragmentManager(); if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else sameFragment = false; setLastSuccessfullySearchedBusStop(p); if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); String displayStuff; if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } attachFragmentToContainer(fm,arrivalsFragment,true,ResultListFragment.getFragmentTag(p)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); } final PalinaAdapter adapter = new PalinaAdapter(act.getApplicationContext(), p); // DO NOT CALL `setListAdapter` ever on arrivals fragment - arrivalsFragment.updateFragmentData(p, adapter); + arrivalsFragment.updateFragmentData(p); act.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 createFragmentFor(List resultList,String query){ act.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query); listfragment.setStopList(resultList); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ if (act instanceof FragmentListener) ((FragmentListener) act).toggleSpinner(on); else { SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID); srl.setRefreshing(false); } } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param sendToSecondaryFrame needs to be displayed in secondary frame or not * @param tag tag for the fragment */ public void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){ FragmentTransaction ft = fm.beginTransaction(); if(sendToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) ft.replace(secondaryFrameLayout,fragment,tag); else ft.replace(primaryFrameLayout,fragment,tag); ft.addToBackStack("state_"+tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); ft.commit(); //fm.executePendingTransactions(); } synchronized public int insertBatchDataInNextGenDB(ContentValues[] valuesArr,String tableName){ if(newDBHelper !=null) try { return newDBHelper.insertBatchContent(valuesArr, tableName); } catch (SQLiteException exc){ Log.w("DB Batch inserting: ","ERROR Inserting the data batch: ",exc.fillInStackTrace()); return -2; } else return -1; } synchronized public ContentResolver getContentResolver(){ return act.getContentResolver(); } public void setBlockAllActivities(boolean shouldI) { this.shouldHaltAllActivities = shouldI; } public void stopLastRequestIfNeeded(){ if(lastTaskRef == null) return; AsyncDataDownload task = lastTaskRef.get(); if(task!=null){ task.cancel(true); } } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.result res){ //TODO: implement a common set of errors for all fragments switch (res){ case OK: break; case CLIENT_OFFLINE: act.showMessage(R.string.network_error); break; case SERVER_ERROR: if (act.isConnected()) { act.showMessage(R.string.parsing_error); } else { act.showMessage(R.string.network_error); } case PARSER_ERROR: default: act.showMessage(R.string.internal_error); break; case QUERY_TOO_SHORT: act.showMessage(R.string.query_too_short); break; case EMPTY_RESULT_SET: act.showMessage(R.string.no_bus_stop_have_this_name); break; } } }