diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java index 88d8ac9..ff8a416 100644 --- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -1,146 +1,147 @@ /* 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 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 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; /** * 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 { 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 static final int cityIcon = R.drawable.city; // hey look, a pattern! private static class ViewHolder { TextView rowStopIcon; TextView rowRouteDestination; TextView rowRouteTimetable; } public PalinaAdapter(Context context, Palina p) { super(context, row_layout, p.queryAllRoutes()); li = LayoutInflater.from(context); } /** * 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); } 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); vh.rowRouteDestination.setText(route.destinazione); } 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; } } diff --git a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java index 9844d47..89d63c1 100644 --- a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java +++ b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java @@ -1,58 +1,56 @@ /* 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; // "arrivals" è più usato di "transit" o simili, e chi sono io per mettermi a dibattere con gli inglesi? -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.atomic.AtomicReference; public interface ArrivalsFetcher extends Fetcher { // /** // * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. // * Don't call this in UI thread! // * // * @param stopID stop ID, in normalized form. // * @param routeID route ID, in normalized form. // * @param res result code (will be set by this method) // * @return arrival times // * @see it.reyboz.bustorino.backend.Fetcher.result // * @see FiveTNormalizer // */ // Palina ReadArrivalTimesRoute(String stopID, String routeID, AtomicReference res); /** * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. * Don't call this in UI thread! * * @param stopID stop ID, in normalized form. * @param res result code (will be set by this method) * @return arrival times - * @see it.reyboz.bustorino.backend.Fetcher.result + * @see Result * @see FiveTNormalizer */ - Palina ReadArrivalTimesAll(String stopID, AtomicReference res); + Palina ReadArrivalTimesAll(String stopID, AtomicReference res); /** * Get the determined source for the Fetcher * @return the source of the arrival times */ Passaggio.Source getSourceForFetcher(); } diff --git a/src/it/reyboz/bustorino/backend/Fetcher.java b/src/it/reyboz/bustorino/backend/Fetcher.java index 3477457..8f1ef20 100644 --- a/src/it/reyboz/bustorino/backend/Fetcher.java +++ b/src/it/reyboz/bustorino/backend/Fetcher.java @@ -1,36 +1,36 @@ /* 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
* QUERY_TOO_SHORT: input more characters and retry. */ - enum result { + enum Result { OK, CLIENT_OFFLINE, SERVER_ERROR, SETUP_ERROR,PARSER_ERROR, EMPTY_RESULT_SET, QUERY_TOO_SHORT,SERVER_ERROR_404 } } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java index a51f48c..b744072 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,396 +1,411 @@ /* 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 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) { + 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 hack is not working anymore"); - res.set(result.EMPTY_RESULT_SET); + 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); } return p; } - try { - List routes = parseArrivalsServerResponse(response, res); - for(Route r: routes){ + List routes = parseArrivalsServerResponse(response, res); + if(res.get()==Result.OK) { + for (Route r : routes) { p.addRoute(r); } - } catch (JSONException ex){ - res.set(result.PARSER_ERROR); - Log.w(DEBUG_NAME, "Couldn't get the JSON repr of:\n"+response); - return null; + p.sortRoutes(); } - res.set(result.OK); - p.sortRoutes(); return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.FiveTAPI; } - List parseArrivalsServerResponse(String JSONresponse, AtomicReference res) throws JSONException{ + 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 routetype = Route.Type.UNKNOWN; for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{ if(response == 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; - if(direction.contains("-")){ + //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) - direction = direction.split("-")[1]; + preParenthesis = preParenthesis.split("-")[1]; } - Route r = new Route(lineName.trim(),direction.trim(),t,new ArrayList<>()); + 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(); - r.setStopsList(Arrays.asList(stops.split(","))); + //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) { + public List getDirectionsForStop(String stopID, AtomicReference res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); List routes; try{ routes = parseDirectionsFromResponse(response); - res.set(result.OK); + res.set(Result.OK); } catch (JSONException | IllegalArgumentException e) { e.printStackTrace(); - res.set(result.PARSER_ERROR); + res.set(Result.PARSER_ERROR); routes = null; } return routes; } - public ArrayList getAllStopsFromGTT(AtomicReference res){ + 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 = responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i getAllLinesFromGTT(AtomicReference res){ + public ArrayList 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){ + 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); + res.set(Result.PARSER_ERROR); return null; } String response = networkTools.queryURL(u,res,param); return response; } /** * 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/FiveTAPIVolleyRequest.java b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java index 46391da..b667be8 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java @@ -1,130 +1,130 @@ /* BusTO - Backend components Copyright (C) 2019 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 com.android.volley.*; import com.android.volley.toolbox.HttpHeaderParser; import org.json.JSONException; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** * Class to handle request with the Volley Library */ public class FiveTAPIVolleyRequest extends Request { private static final String LOG_TAG = "BusTO-FiveTAPIVolleyReq"; private ResponseListener listener; final private String url,stopID; - final private AtomicReference resultRef; + final private AtomicReference resultRef; final private FiveTAPIFetcher fetcher; final private FiveTAPIFetcher.QueryType type; private FiveTAPIVolleyRequest(int method, String url, String stopID, FiveTAPIFetcher.QueryType kind, ResponseListener listener, @Nullable Response.ErrorListener errorListener) { super(method, url, errorListener); this.url = url; this.resultRef = new AtomicReference<>(); this.fetcher = new FiveTAPIFetcher(); this.listener = listener; this.stopID = stopID; this.type = kind; } @Nullable public static FiveTAPIVolleyRequest getNewRequest(FiveTAPIFetcher.QueryType type, String stopID, ResponseListener listener, @Nullable Response.ErrorListener errorListener){ String url; try { url = FiveTAPIFetcher.getURLForOperation(type,stopID); } catch (UnsupportedEncodingException e) { e.printStackTrace(); Log.e(LOG_TAG,"Cannot get an URL for the operation"); return null; } return new FiveTAPIVolleyRequest(Method.GET,url,stopID,type,listener,errorListener); } @Override protected Response parseNetworkResponse(NetworkResponse response) { if(response.statusCode != 200) return Response.error(new VolleyError("Response Error Code "+response.statusCode)); final String stringResponse = new String(response.data); List routeList; try{ switch (type){ case ARRIVALS: routeList = fetcher.parseArrivalsServerResponse(stringResponse,resultRef); break; case DETAILS: routeList = fetcher.parseDirectionsFromResponse(stringResponse); break; default: //empty return Response.error(new VolleyError("Invalid query type")); } } catch (JSONException e) { - resultRef.set(Fetcher.result.PARSER_ERROR); + resultRef.set(Fetcher.Result.PARSER_ERROR); //e.printStackTrace(); Log.w("FivetVolleyParser","JSON Exception in parsing response of: "+url); return Response.error(new ParseError(response)); } - if(resultRef.get()== Fetcher.result.PARSER_ERROR){ + if(resultRef.get()== Fetcher.Result.PARSER_ERROR){ return Response.error(new ParseError(response)); } final Palina p = new Palina(stopID); p.setRoutes(routeList); p.sortRoutes(); return Response.success(p, HttpHeaderParser.parseCacheHeaders(response)); } @Override protected void deliverResponse(Palina p) { listener.onResponse(p,type); } @Override public Map getHeaders() { return FiveTAPIFetcher.getDefaultHeaders(); } //from https://stackoverflow.com/questions/21867929/android-how-handle-message-error-from-the-server-using-volley @Override protected VolleyError parseNetworkError(VolleyError volleyError){ if(volleyError.networkResponse != null && volleyError.networkResponse.data != null){ volleyError = new NetworkError(volleyError.networkResponse); } return volleyError; } public interface ResponseListener{ void onResponse(Palina result, FiveTAPIFetcher.QueryType type); } //public interface ErrorListener extends Response.ErrorListener{} } diff --git a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java index 5c4152f..4d482e3 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) { + 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); + 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); + 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.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); + 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); + 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); + 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); + 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); + 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/FiveTStopsFetcher.java b/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java index 6c6dca9..e5bc001 100644 --- a/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java @@ -1,119 +1,119 @@ /* 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 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.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * Once was asynchronous BusStop[] fetcher from a query, code mostly taken from * AsyncWgetBusStopSuggestions (by Valerio Bozzolan) * * @see FiveTScraperFetcher */ public class FiveTStopsFetcher implements StopsFinderByName { @Override - public List FindByName(String name, AtomicReference res) { + public List FindByName(String name, AtomicReference res) { // API apparently limited to 20 results ArrayList busStops = new ArrayList<>(20); String stopID; String stopName; String stopLocation; //Stop busStop; if(name.length() < 3) { - res.set(result.QUERY_TOO_SHORT); + res.set(Result.QUERY_TOO_SHORT); return busStops; } String responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas; URL u; try { u = new URL("http://www.5t.torino.it/5t/trasporto/stop-lookup.jsp?action=search&stopShortName=" + URLEncoder.encode(name, "utf-8")); } catch(Exception e) { - res.set(Fetcher.result.PARSER_ERROR); + res.set(Result.PARSER_ERROR); return busStops; } responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = networkTools.getDOM(u, res); if (responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas == null) { // result already set in getDOM() return busStops; } Document doc = Jsoup.parse(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas); // Find bus stops Elements lis = doc.getElementsByTag("li"); for(Element li : lis) { Elements spans = li.getElementsByTag("span"); // busStopID try { stopID = FiveTNormalizer.FiveTNormalizeRoute(spans.eq(0).text()); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopID"); stopID = ""; } // busStopName try { stopName = spans.eq(1).text(); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopName"); stopName = ""; } // busStopLocation try { stopLocation = (spans.eq(2).text()); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopLocation"); stopLocation = null; } /* if(stopLocation == null || stopLocation.length() == 0) { stopLocation = db.getLocationFromID(stopID); }*/ busStops.add(new Stop(stopName, stopID, stopLocation, null, null)); } if(busStops.size() == 0) { - res.set(result.EMPTY_RESULT_SET); + res.set(Result.EMPTY_RESULT_SET); } else { - res.set(result.OK); + res.set(Result.OK); } Collections.sort(busStops); // TODO: remove duplicates? (see GTTStopsFetcher) return busStops; } } diff --git a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java index ca14f48..112eca6 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) { + 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); + 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); + 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.EMPTY_RESULT_SET); return p; } howManyRoutes = json.length(); if(howManyRoutes == 0) { - res.set(result.EMPTY_RESULT_SET); + 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); + res.set(Result.PARSER_ERROR); return p; } p.sortRoutes(); - res.set(result.OK); + res.set(Result.OK); return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.GTTJSON; } } diff --git a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java index 1bde9dd..89836af 100644 --- a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java @@ -1,191 +1,191 @@ /* 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 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.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; public class GTTStopsFetcher implements StopsFinderByName { @Override @NonNull - public List FindByName(String name, AtomicReference res) { + public List FindByName(String name, AtomicReference res) { URL url; // sorting an ArrayList should be faster than a LinkedList and the API is limited to 15 results List s = new ArrayList<>(15); List s2 = new ArrayList<>(15); String fullname; String content; String bacino; String localita; Route.Type type; JSONArray json; int howManyStops, i; JSONObject thisstop; if(name.length() < 3) { - res.set(result.QUERY_TOO_SHORT); + res.set(Result.QUERY_TOO_SHORT); return s; } try { url = new URL("http://www.gtt.to.it/cms/components/com_gtt/views/palinejson/view.html.php?term=" + URLEncoder.encode(name, "utf-8")); } catch (Exception e) { - res.set(result.PARSER_ERROR); + res.set(Result.PARSER_ERROR); return s; } content = networkTools.queryURL(url, res); if(content == null) { return s; } try { json = new JSONArray(content); } catch(JSONException e) { if(content.contains("[]")) { // when no results are found, server returns a PHP Warning and an empty array. In case they fix the warning, we're looking for the array. - res.set(result.EMPTY_RESULT_SET); + res.set(Result.EMPTY_RESULT_SET); } else { - res.set(result.PARSER_ERROR); + res.set(Result.PARSER_ERROR); } return s; } howManyStops = json.length(); if(howManyStops == 0) { - res.set(result.EMPTY_RESULT_SET); + res.set(Result.EMPTY_RESULT_SET); return s; } try { for(i = 0; i < howManyStops; i++) { thisstop = json.getJSONObject(i); fullname = thisstop.getString("data"); String ID = thisstop.getString("value"); try { localita = thisstop.getString("localita"); if(localita.equals("[MISSING]")) { localita = null; } } catch(JSONException e) { localita = null; } /* if(localita == null || localita.length() == 0) { localita = db.getLocationFromID(ID); } //TODO: find località by ContentProvider */ try { bacino = thisstop.getString("bacino"); } catch (JSONException ignored) { bacino = "U"; } if(fullname.startsWith("Metro ")) { type = Route.Type.METRO; } else if(fullname.length() >= 6 && fullname.startsWith("S00")) { type = Route.Type.RAILWAY; } else if(fullname.startsWith("ST")) { type = Route.Type.RAILWAY; } else { type = FiveTNormalizer.decodeType("", bacino); } //TODO: refactor using content provider s.add(new Stop(fullname, ID, localita, type,null)); } } catch (JSONException e) { - res.set(result.PARSER_ERROR); + res.set(Result.PARSER_ERROR); return s; } if(s.size() < 1) { // shouldn't happen but prevents the next part from catching fire - res.set(result.EMPTY_RESULT_SET); + res.set(Result.EMPTY_RESULT_SET); return s; } Collections.sort(s); // the next loop won't work with less than 2 items if(s.size() < 2) { - res.set(result.OK); + res.set(Result.OK); return s; } /* There are some duplicate stops returned by this API. * Long distance buses have stop IDs with 5 digits. Always. They are zero-padded if there * aren't enough. E.g. stop 631 becomes 00631. * * Unfortunately you can't use padded stops to query any API. * Fortunately, unpadded stops return both normal and long distance bus timetables. * FiveTNormalizer is already removing padding (there may be some padded stops for which the * API doesn't return an unpadded equivalent), here we'll remove duplicates by skipping * padded stops, which also never have a location. * * I had to draw a finite state machine on a piece of paper to understand how to implement * this loop. */ for(i = 1; i < howManyStops; ) { Stop current = s.get(i); Stop previous = s.get(i-1); // same stop: let's see which one to keep... if(current.ID.equals(previous.ID)) { if(previous.location == null) { // previous one is useless: discard it, increment i++; } else if(current.location == null) { // this one is useless: add previous and skip one s2.add(previous); i += 2; } else { // they aren't really identical: to err on the side of caution, keep them both. s2.add(previous); i++; } } else { // different: add previous, increment s2.add(previous); i++; } } // unless the last one was garbage (i would be howManyStops+1 in that case), add it if(i == howManyStops) { s2.add(s.get(i-1)); } - res.set(result.OK); + res.set(Result.OK); return s2; } } diff --git a/src/it/reyboz/bustorino/backend/StopsFinderByName.java b/src/it/reyboz/bustorino/backend/StopsFinderByName.java index b30a9ed..f1d9a27 100644 --- a/src/it/reyboz/bustorino/backend/StopsFinderByName.java +++ b/src/it/reyboz/bustorino/backend/StopsFinderByName.java @@ -1,33 +1,33 @@ /* 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 java.util.List; import java.util.concurrent.atomic.AtomicReference; public interface StopsFinderByName extends Fetcher { /** * Finds stops by name. Don't call this in UI thread! * * @param name the string to search for * @return list of stops, in normalized form. * @see FiveTNormalizer */ - List FindByName(String name, AtomicReference res); + List FindByName(String name, AtomicReference res); } diff --git a/src/it/reyboz/bustorino/backend/StopsFinderByRoute.java b/src/it/reyboz/bustorino/backend/StopsFinderByRoute.java index d11dd25..5d99fdb 100644 --- a/src/it/reyboz/bustorino/backend/StopsFinderByRoute.java +++ b/src/it/reyboz/bustorino/backend/StopsFinderByRoute.java @@ -1,33 +1,33 @@ /* 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 java.util.List; import java.util.concurrent.atomic.AtomicReference; public interface StopsFinderByRoute extends Fetcher { /** * Finds every stop in a route. Don't call this in UI thread! * * @param routeID route ID, in normalized form. * @return list of stops, in normalized form. * @see FiveTNormalizer */ - List FindByRoute(String routeID, StopsDBInterface db, AtomicReference res); + List FindByRoute(String routeID, StopsDBInterface db, AtomicReference res); } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java index d0285aa..4856239 100644 --- a/src/it/reyboz/bustorino/backend/networkTools.java +++ b/src/it/reyboz/bustorino/backend/networkTools.java @@ -1,180 +1,180 @@ /* 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 androidx.annotation.Nullable; import android.util.Log; import java.io.BufferedInputStream; 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.util.Map; import java.util.Scanner; import java.util.concurrent.atomic.AtomicReference; public abstract class networkTools { - static String getDOM(final URL url, final AtomicReference res) { + static String getDOM(final URL url, final AtomicReference res) { //Log.d("asyncwget", "Catching URL in background: " + uri[0]); HttpURLConnection urlConnection; StringBuilder result = null; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { - res.set(Fetcher.result.SERVER_ERROR); + res.set(Fetcher.Result.SERVER_ERROR); return null; } try { InputStream in = new BufferedInputStream( urlConnection.getInputStream()); BufferedReader reader = new BufferedReader( new InputStreamReader(in)); result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line); } } catch (Exception e) { //Log.e("asyncwget", e.getMessage()); } finally { if (urlConnection != null) { urlConnection.disconnect(); } } if (result == null) { - res.set(Fetcher.result.SERVER_ERROR); + res.set(Fetcher.Result.SERVER_ERROR); return null; } - res.set(Fetcher.result.PARSER_ERROR); // will be set to "OK" later, this is a safety net in case StringBuilder returns null, the website returns an HTTP 204 or something like that. + res.set(Fetcher.Result.PARSER_ERROR); // will be set to "OK" later, this is a safety net in case StringBuilder returns null, the website returns an HTTP 204 or something like that. return result.toString(); } @Nullable - static String queryURL(URL url, AtomicReference res){ + static String queryURL(URL url, AtomicReference res){ return queryURL(url,res,null); } @Nullable - static String queryURL(URL url, AtomicReference res, Map headers) { + static String queryURL(URL url, AtomicReference res, Map headers) { HttpURLConnection urlConnection; InputStream in; String s; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { //e.printStackTrace(); - res.set(Fetcher.result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. + res.set(Fetcher.Result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. return null; } // TODO: make this configurable? urlConnection.setConnectTimeout(3000); urlConnection.setReadTimeout(10000); if(headers!= null){ for(String key : headers.keySet()){ urlConnection.setRequestProperty(key,headers.get(key)); } } - res.set(Fetcher.result.SERVER_ERROR); // will be set to OK later + res.set(Fetcher.Result.SERVER_ERROR); // will be set to OK later try { in = urlConnection.getInputStream(); } catch (Exception e) { try { if(urlConnection.getResponseCode()==404) - res.set(Fetcher.result.SERVER_ERROR_404); + res.set(Fetcher.Result.SERVER_ERROR_404); } catch (IOException e2) { e2.printStackTrace(); } return null; } //s = streamToString(in); try { final long startTime = System.currentTimeMillis(); s = parseStreamToString(in); final long endtime = System.currentTimeMillis(); Log.d("NetworkTools-queryURL","reading response took "+(endtime-startTime)+" millisec"); } catch (IOException e) { e.printStackTrace(); return null; } try { in.close(); } catch(IOException ignored) { //ignored.printStackTrace(); } try { urlConnection.disconnect(); } catch(Exception ignored) { //ignored.printStackTrace(); } if(s.length() == 0) { Log.w("NET TOOLS", "string is empty"); return null; } else { //Log.d("NET TOOLS", s); return s; } } // https://stackoverflow.com/a/5445161 static String streamToString(InputStream is) { Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } /** * New method, maybe faster, to read inputStream * also see https://stackoverflow.com/a/5445161 * @param is what to read * @return the String Read * @throws IOException from the InputStreamReader */ static String parseStreamToString(InputStream is) throws IOException{ final int bufferSize = 1024; final char[] buffer = new char[bufferSize]; final StringBuilder out = new StringBuilder(); InputStreamReader in = new InputStreamReader(is, "UTF-8"); int rsz= in.read(buffer, 0, buffer.length); while( rsz >0) { out.append(buffer, 0, rsz); rsz = in.read(buffer, 0, buffer.length); } return out.toString(); } static int failsafeParseInt(String str) { try { return Integer.parseInt(str); } catch(NumberFormatException e) { return 0; } } } diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index 16e2fa4..3c0f697 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,130 +1,150 @@ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Arrays; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } /* public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } */ public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } + /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } + */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } + public static String toTitleCase(String givenString) { + String[] arr = givenString.split(" "); + StringBuffer sb = new StringBuffer(); + //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); + for (int i = 0; i < arr.length; i++) { + if (arr[i].length() > 1) + sb.append(Character.toUpperCase(arr[i].charAt(0))) + .append(arr[i].substring(1)).append(" "); + else sb.append(arr[i]); + } + return sb.toString().trim(); + } + + /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); context.startActivity(browserIntent1); } + /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ + /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } + */ } diff --git a/src/it/reyboz/bustorino/data/DBUpdateWorker.java b/src/it/reyboz/bustorino/data/DBUpdateWorker.java index 618eeb2..3aaa514 100644 --- a/src/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/src/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,138 +1,138 @@ 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"; 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 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); 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 Result.failure(out); + return ListenableWorker.Result.failure(out); } //we got a good version if (current_DB_version >= new_DB_version && !isUpdateCompulsory) { //don't need to update cancelNotification(notificationID); - return Result.success(new Data.Builder(). + return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } //start the real update - AtomicReference resultAtomicReference = new AtomicReference<>(); + 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(); + 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 Result.failure(dataBuilder.build()); + 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); editor.apply(); cancelNotification(notificationID); - return Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); + 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 be15d95..6e1aaad 100644 --- a/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,158 +1,158 @@ 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 it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; 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 DB_VERSION_KEY = "NextGenDB.GTTVersion"; 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<>(); + 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) { + 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) { + if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading"); - return Result.ERROR_STOPS_DOWNLOAD; + 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(); //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) { 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()); //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 Result.ERROR_LINES_DOWNLOAD; + 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 Result.DONE; + 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(); } } diff --git a/src/it/reyboz/bustorino/data/DatabaseUpdateService.java b/src/it/reyboz/bustorino/data/DatabaseUpdateService.java index aba376f..75cda4a 100644 --- a/src/it/reyboz/bustorino/data/DatabaseUpdateService.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdateService.java @@ -1,278 +1,278 @@ /* 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.app.IntentService; import android.content.*; import androidx.annotation.Nullable; import android.util.Log; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import org.json.JSONException; import org.json.JSONObject; import java.util.concurrent.atomic.AtomicReference; /** * An {@link IntentService} subclass for handling asynchronous task requests in * a service on a separate handler thread. */ public class DatabaseUpdateService extends IntentService { // IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS private static final String ACTION_UPDATE = "it.reyboz.bustorino.middleware.action.UPDATE_DB"; private static final String DB_VERSION = "NextGenDB.GTTVersion"; private static final String DEBUG_TAG = "DatabaseService_BusTO"; // TODO: Rename parameters private static final String TRIAL = "it.reyboz.bustorino.middleware.extra.TRIAL"; private static final String COMPULSORY = "compulsory_update"; private static final int MAX_TRIALS = 5; private static final int VERSION_UNAIVALABLE = -2; public DatabaseUpdateService() { super("DatabaseUpdateService"); } private boolean isRunning; private int updateTrial; /** * Starts this service to perform action Foo with the given parameters. If * the service is already performing a task this action will be queued. * * @see IntentService */ public static void startDBUpdate(Context con, int trial, @Nullable Boolean mustUpdate){ Intent intent = new Intent(con, DatabaseUpdateService.class); intent.setAction(ACTION_UPDATE); intent.putExtra(TRIAL,trial); if(mustUpdate!=null){ intent.putExtra(COMPULSORY,mustUpdate); } con.startService(intent); } public static void startDBUpdate(Context con) { startDBUpdate(con, 0, false); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { final String action = intent.getAction(); if (ACTION_UPDATE.equals(action)) { Log.d(DEBUG_TAG,"Started action update"); SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE); int versionDB = shPr.getInt(DB_VERSION,-1); final int trial = intent.getIntExtra(TRIAL,-1); final SharedPreferences.Editor editor = shPr.edit(); updateTrial = trial; UpdateRequestParams params = new UpdateRequestParams(intent); int newVersion = getNewVersion(params); if(newVersion==VERSION_UNAIVALABLE){ //NOTHING LEFT TO DO return; } Log.d(DEBUG_TAG,"newDBVersion: "+newVersion+" oldVersion: "+versionDB); if(params.mustUpdate || versionDB==-1 || newVersion>versionDB){ Log.d(DEBUG_TAG,"Downloading the bus stops info"); - final AtomicReference gres = new AtomicReference<>(); + final AtomicReference gres = new AtomicReference<>(); if(!performDBUpdate(gres)) restartDBUpdateifPossible(params,gres); else { editor.putInt(DB_VERSION,newVersion); // BY COMMENTING THIS, THE APP WILL CONTINUOUSLY UPDATE THE DATABASE editor.apply(); } } else { Log.d(DEBUG_TAG,"No update needed"); } Log.d(DEBUG_TAG,"Finished update"); setDBUpdatingFlag(shPr,false); } } } private boolean setDBUpdatingFlag(SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(getString(R.string.databaseUpdatingPref),value); return editor.commit(); } private boolean setDBUpdatingFlag(boolean value){ final SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(shPr,value); } - private boolean performDBUpdate(AtomicReference gres){ + private boolean performDBUpdate(AtomicReference gres){ if(!setDBUpdatingFlag(true)) return false; /* 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 false; } if(!setDBUpdatingFlag(true)) return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database final NextGenDB dbHelp = new NextGenDB(getApplicationContext()); final SQLiteDatabase db = dbHelp.getWritableDatabase(); //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) { final ContentValues cv = new ContentValues(); cv.put(StopsTable.COL_ID, s.ID); cv.put(StopsTable.COL_NAME, s.getStopDefaultName()); if (s.location != null) cv.put(StopsTable.COL_LOCATION, s.location); cv.put(StopsTable.COL_LAT, s.getLatitude()); cv.put(StopsTable.COL_LONG, s.getLongitude()); if (s.getAbsurdGTTPlaceName() != null) cv.put(StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName()); cv.put(StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString()); if (s.type != null) cv.put(StopsTable.COL_TYPE, s.type.getCode()); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(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 false; } db.beginTransaction(); startTime = System.currentTimeMillis(); for (Route r: routes){ final ContentValues cv = new ContentValues(); cv.put(LinesTable.COLUMN_NAME,r.getName()); switch (r.type){ case BUS: cv.put(LinesTable.COLUMN_TYPE,"URBANO"); break; case RAILWAY: cv.put(LinesTable.COLUMN_TYPE,"FERROVIA"); break; case LONG_DISTANCE_BUS: cv.put(LinesTable.COLUMN_TYPE,"EXTRA"); break; } cv.put(LinesTable.COLUMN_DESCRIPTION,r.description); //db.insert(LinesTable.TABLE_NAME,null,cv); int rows = db.update(LinesTable.TABLE_NAME,cv,LinesTable.COLUMN_NAME+" = ?",new String[]{r.getName()}); if(rows<1){ //we haven't changed anything db.insert(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 true; */ return DatabaseUpdate.performDBUpdate(getApplication(),gres) == DatabaseUpdate.Result.DONE; } private int getNewVersion(UpdateRequestParams params){ - AtomicReference gres = new AtomicReference<>(); + AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ restartDBUpdateifPossible(params,gres); return VERSION_UNAIVALABLE; } boolean needed; 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 -4; } } - private void restartDBUpdateifPossible(UpdateRequestParams pars, AtomicReference res){ - if (pars.trial res){ + if (pars.trial. */ 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.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; private final static ArrivalsFetcher[] defaultFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; 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(defaultFetchers)); 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()]; 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 UNDETERMINED: //Don't show the view timesSourceTextView.setVisibility(View.GONE); return; default: throw new IllegalStateException("Unexpected value: " + source); } int count = 0; 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 4ff4e53..3f265a4 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,267 +1,268 @@ /* 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.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 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; } /** * 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; 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?"); assert arrivalsFragment != null; sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else { sameFragment = false; Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); } 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); } String probableTag = ResultListFragment.getFragmentTag(p); attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); } // 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); 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){ 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); - ft.commit(); + if(!fm.isDestroyed()) + ft.commit(); //fm.executePendingTransactions(); } 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){ + public void showErrorMessage(Fetcher.Result res){ //TODO: implement a common set of errors for all fragments 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: 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 02c4258..a055cff 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,699 +1,700 @@ + 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.ArrivalsFetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.FiveTStopsFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.GTTStopsFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.StopsFinderByName; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncDataDownload; 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 = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { 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 new AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(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; 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()){ 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) && 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); } 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); } } */ } @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.toString() + " 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); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); } /* 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(); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).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); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = fragMan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = fragMan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(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 } /** * 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); } else{ new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { new AsyncDataDownload(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/AsyncDataDownload.java index cb33819..8c5d52d 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,347 +1,356 @@ /* 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.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 android.util.Log; import it.reyboz.bustorino.backend.*; 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 AsyncDataDownload 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 AtomicReference res; private final RequestType t; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final Fetcher[] theFetchers; private Context context; private final boolean replaceFragment; public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) { RequestType type; helperRef = new WeakReference<>(fh); fh.setLastTaskRef(new WeakReference<>(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; FragmentHelper fh = helperRef.get(); //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(); 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); + publishProgress(Fetcher.Result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times if(f instanceof FiveTAPIFetcher && Integer.parseInt(stopID)>= 8200) continue; p= f.ReadArrivalTimesAll(stopID,res); publishProgress(res.get()); + //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<>(); + AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); - if(gres.get() == Fetcher.result.OK){ + 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 } - if(lastSearchedBusStop != null && res.get()== Fetcher.result.OK) { + 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); } } } 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 break; default: result = null; } //find if it went well - if(res.get()== Fetcher.result.OK) { + if(res.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return result; } } //at this point, we are sure that the result has been negative failedAll=true; return null; } @Override - protected void onProgressUpdate(Fetcher.result... values) { + protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) - for (Fetcher.result r : values){ + for (Fetcher.Result r : values){ //TODO: make Toast fh.showErrorMessage(r); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Object o) { FragmentHelper fh = helperRef.get(); if(failedAll || o == 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); } 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; } } @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()]; + //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; - 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(); } } }