diff --git a/app/src/main/java/it/reyboz/bustorino/backend/ArrivalsFetcherContext.java b/app/src/main/java/it/reyboz/bustorino/backend/ArrivalsFetcherContext.java new file mode 100644 index 0000000..5d08c94 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/ArrivalsFetcherContext.java @@ -0,0 +1,14 @@ +package it.reyboz.bustorino.backend; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public abstract class ArrivalsFetcherContext implements ArrivalsFetcher{ + + protected @Nullable Context appContext; + + public void setContext(@NonNull Context appContext) { + this.appContext = appContext.getApplicationContext(); + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/app/src/main/java/it/reyboz/bustorino/backend/GTTJSONFetcher.java index 039465d..b94fe38 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/GTTJSONFetcher.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/GTTJSONFetcher.java @@ -1,136 +1,225 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi + Copyright (C) 2026 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.volley.*; +import com.android.volley.toolbox.HttpHeaderParser; +import com.android.volley.toolbox.RequestFuture; 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.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; -public class GTTJSONFetcher implements ArrivalsFetcher { +import static java.util.concurrent.TimeUnit.SECONDS; + +public class GTTJSONFetcher extends ArrivalsFetcherContext { private final String DEBUG_TAG = "GTTJSONFetcher-BusTO"; @Override @NonNull public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { URL url; Palina p = new Palina(stopID); - String routename; - String bacino; - String content; - JSONArray json; - int howManyRoutes, howManyPassaggi, i, j, pos; // il misto inglese-italiano è un po' ridicolo ma tanto vale... - JSONObject thisroute; - JSONArray passaggi; try { url = new URL("https://www.gtt.to.it/cms/index.php?option=com_gtt&task=palina.getTransitiOld&palina=" + URLEncoder.encode(stopID, "utf-8") + "&bacino=U&realtime=true&get_param=value"); } catch (Exception e) { res.set(Result.PARSER_ERROR); return p; } - HashMap headers = new HashMap<>(); - //headers.put("Referer","https://www.gtt.to.it/cms/percorari/urbano?view=percorsi&bacino=U&linea=15&Regol=GE"); - headers.put("Host", "www.gtt.to.it"); - content = networkTools.queryURL(url, res, headers); + + /*content = networkTools.queryURL(url, res, headers); if(content == null) { Log.w("GTTJSONFetcher", "NULL CONTENT"); return p; } try { json = new JSONArray(content); } catch(JSONException e) { Log.w(DEBUG_TAG, "Error parsing JSON: \n"+content); Log.w(DEBUG_TAG, e); res.set(Result.PARSER_ERROR); return p; } - try { - // returns [{"PassaggiRT":[],"Passaggi":[]}] for non existing stops! - json.getJSONObject(0).getString("Linea"); // if we can get this, then there's something useful in the array. - } catch(JSONException e) { - Log.w(DEBUG_TAG, "No existing lines"); - res.set(Result.NOT_FOUND); - return p; - } - - howManyRoutes = json.length(); - if(howManyRoutes == 0) { - res.set(Result.EMPTY_RESULT_SET); + */ + if (appContext == null) { + Log.w(DEBUG_TAG, "appContext is null"); + res.set(Result.PARSER_ERROR); 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"; - } - final Route r = new Route(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("_", ""); - } - r.addPassaggio(mPassaggio.concat("*"), Passaggio.Source.GTTJSON); - } - + boolean retry = true; + RequestQueue queue = NetworkVolleyManager.getInstance(appContext).getRequestQueue(); + //use the volley class, max 5 tries + RequestFuture future; + Request request; + Response.ErrorListener responder = error -> { + //Log.w(DEBUG_TAG, "onErrorResponse: " + volleyError.getMessage()); + if(error instanceof VolleyFetcherError){ + Log.w(DEBUG_TAG, "Actual error: " + ((VolleyFetcherError) error).getReason()); + } + }; + + for (int i = 0; i < 2; i++) { + future = RequestFuture.newFuture(); + request = new GTTRequest(stopID, url.toString(), responder, future, res); + + queue.add(request); + + try { + p = future.get(10, SECONDS); + retry = false; + } catch (TimeoutException e) { + Log.d(DEBUG_TAG, "Request timed out: " + res.get()); + retry = false; + res.set(Result.CONNECTION_ERROR); + } catch (InterruptedException | ExecutionException e) { + Log.w(DEBUG_TAG, "Error: " + e + " status: " + res.get()); + res.set(Result.PARSER_ERROR); + } - passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones - howManyPassaggi = passaggi.length(); - for(j = 0; j < howManyPassaggi; j++) { - r.addPassaggio(passaggi.getString(j), Passaggio.Source.GTTJSON); - } - p.addRoute(r); + if(!retry){ + break; } - } catch (JSONException e) { - res.set(Result.PARSER_ERROR); - e.printStackTrace(); - return p; } - p.sortRoutes(); - res.set(Result.OK); return p; } + @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.GTTJSON; } + + private final class GTTRequest extends Request { + private final String stopID; + private final AtomicReference res; + private final Response.Listener responder; + + public GTTRequest(String stopID, String URL, + @Nullable Response.ErrorListener errorListener, + Response.Listener resp, + AtomicReference resu) { + super(Method.GET, URL, errorListener); + this.stopID = stopID; + this.res = resu; + responder = resp; + } + @Override + protected Response parseNetworkResponse(NetworkResponse networkResponse) { + if (networkResponse == null) { + return Response.error(new VolleyFetcherError(Result.PARSER_ERROR)); + } + + String data = new String(networkResponse.data); + JSONArray json; + try { + json = new JSONArray(data); + // 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.NOT_FOUND); + return Response.error(new VolleyFetcherError(Result.NOT_FOUND)); + } + + int howManyRoutes = json.length(); + if(howManyRoutes == 0) { + res.set(Result.EMPTY_RESULT_SET); + return Response.error(new VolleyFetcherError(Result.EMPTY_RESULT_SET)); + } + + try { + JSONObject thisroute; + String routename, bacino; + JSONArray passaggi; + int howManyPassaggi; + Palina p = new Palina(stopID); + for(int 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"; + } + final Route r = new Route(routename, thisroute.getString("Direzione"), + "", + FiveTNormalizer.decodeType(routename, bacino)); + + passaggi = thisroute.getJSONArray("PassaggiRT"); + howManyPassaggi = passaggi.length(); + for(int j = 0; j < howManyPassaggi; j++) { + String mPassaggio = passaggi.getString(j); + if (mPassaggio.contains("__")){ + mPassaggio = mPassaggio.replace("_", ""); + } + r.addPassaggio(mPassaggio.concat("*"), Passaggio.Source.GTTJSON); + } + + + passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones + howManyPassaggi = passaggi.length(); + for(int j = 0; j < howManyPassaggi; j++) { + r.addPassaggio(passaggi.getString(j), Passaggio.Source.GTTJSON); + } + p.addRoute(r); + } + p.sortRoutes(); + res.set(Result.OK); + + return Response.success(p, HttpHeaderParser.parseCacheHeaders(networkResponse)); + } catch (JSONException e) { + res.set(Result.PARSER_ERROR); + Log.d(DEBUG_TAG, "Failed to parse response into JSON: " + e.getMessage()); + return Response.error(new VolleyFetcherError(Result.PARSER_ERROR)); + } + } + + @Override + public Map getHeaders() { + 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"); + return headers; + } + + @Override + protected void deliverResponse(Palina palina) { + responder.onResponse(palina); + } + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherError.kt b/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherError.kt new file mode 100644 index 0000000..74d4a27 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherError.kt @@ -0,0 +1,7 @@ +package it.reyboz.bustorino.backend + +import com.android.volley.VolleyError + +class VolleyFetcherError( + val reason: Fetcher.Result +) : VolleyError() \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherErrorResponder.kt b/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherErrorResponder.kt new file mode 100644 index 0000000..1e79cbd --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/VolleyFetcherErrorResponder.kt @@ -0,0 +1,23 @@ +package it.reyboz.bustorino.backend + +import android.util.Log +import com.android.volley.Response +import com.android.volley.VolleyError + +interface VolleyFetcherErrorResponder: Response.ErrorListener { + + fun onErrorResponse(error: VolleyFetcherError){ + + } + + override fun onErrorResponse(p0: VolleyError?) { + p0.let { + if(p0 is VolleyFetcherError){ + onErrorResponse(p0 as VolleyFetcherError) + } + else{ + Log.e("VolleyFetcherError", "Error is not instance of VolleyFetcherError, ignoring") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index 9d0de14..664f2dc 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,430 +1,427 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.gtfs.GtfsAgency import it.reyboz.bustorino.data.gtfs.GtfsFeed import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPattern import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList open class MatoAPIFetcher( private val minNumPassaggi: Int -) : ArrivalsFetcher { - var appContext: Context? = null - set(value) { - field = value!!.applicationContext - } +) : ArrivalsFetcherContext() { + constructor(): this(DEF_MIN_NUMPASSAGGI) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val now = Calendar.getInstance().time var numMinutes = 60 var palina = Palina(stopID) var trials = 0 val numDepartures = 8 var moreTime = false var palinaOK = false while (trials <20 && !palinaOK) { //numDepartures+=2 if (moreTime) numMinutes *= 2 //duplicate time val future = RequestFuture.newFuture() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) moreTime = false try { val palinaResult = future.get(15, TimeUnit.SECONDS) if (palinaResult!=null) { palina = palinaResult if(palina.totalNumberOfPassages < minNumPassaggi && numMinutes < MAX_MINUTES_SEARCH) { moreTime = true } else{ palinaOK = true } } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" const val DEF_MIN_NUMPASSAGGI = 5 const val MAX_MINUTES_SEARCH = 24*60 // a day in minutes val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" MatoQueries.QueryType.TRIP -> VOLLEY_TAG+"_Trip" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude, jsonStop.getString("gtfsId") ) val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.gtfsId = routeBaseInfo.getString("gtfsId").trim() baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } private fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") val passaggio = Passaggio.newInstance(realtimeTime, realtime, (realtimeTime-scheduledTime), Passaggio.Source.MatoAPI) passaggio?.let{ passages.add(it) } /*passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) */ } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( FiveTNormalizer.filterFullStarName(routeJSON.getString("shortName")), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) return data } fun getFeedsAndAgencies(context: Context, res: AtomicReference?): Pair, ArrayList> { val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) request.setRetryPolicy(longRetryPolicy) request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) requestQueue.add(request) val feeds = ArrayList() val agencies = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val feedsJSON = resObj.getJSONArray("feeds") for (i in 0 until feedsJSON.length()){ val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) feeds.add(resTup.first) agencies.addAll(resTup.second) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return Pair(feeds,agencies) } fun getRoutes(context: Context, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() params.put("feeds","gtt") val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) request.retryPolicy = longRetryPolicy requestQueue.add(request) val routes = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) routes.add(route) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return routes } fun getPatternsWithStops(context: Context, routesGTFSIds: MutableCollection, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() for (r in routesGTFSIds){ if(r.isEmpty()) routesGTFSIds.remove(r) } val routes = JSONArray(routesGTFSIds) params.put("routes",routes) val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) request.retryPolicy = longRetryPolicy request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) requestQueue.add(request) val patterns = ArrayList() var resObj = JSONObject() try { resObj = future.get(60,TimeUnit.SECONDS) //outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) patterns.addAll(patternList) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Got result: $resObj") } return patterns } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java index 709bc42..623d2a1 100644 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java +++ b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java @@ -1,339 +1,339 @@ /* BusTO (middleware) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.middleware; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; import android.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; import android.util.Log; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.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 AsyncArrivalsSearcher extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference finalResultRef; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final ArrivalsFetcher[] theFetchers; @SuppressLint("StaticFieldLeak") private final Context context; private final boolean replaceFragment; public AsyncArrivalsSearcher(FragmentHelper fh, @NonNull ArrivalsFetcher[] fetchers, Context context) { helperRef = new WeakReference<>(fh); fh.setLastTaskRef(this); finalResultRef = 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!"); } } @Override protected Palina doInBackground(String... params) { RecursionHelper r = new RecursionHelper<>(theFetchers); Palina resultPalina = null; FragmentHelper fh = helperRef.get(); ArrayList results = new ArrayList<>(theFetchers.length); //If the FragmentHelper is null, that means the activity doesn't exist anymore StringBuilder sb = new StringBuilder(); for (ArrivalsFetcher f: theFetchers){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } Log.d(DEBUG_TAG, "Using fetchers: "+sb.toString()); 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 ArrivalsFetcher f = r.getAndMoveForward(); AtomicReference resRef = new AtomicReference<>(); - if (f instanceof MatoAPIFetcher){ - ((MatoAPIFetcher)f).setAppContext(context); + if (f instanceof ArrivalsFetcherContext){ + ((ArrivalsFetcherContext)f).setContext(context); } Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); Palina p; String stopID; if(params.length>0) stopID=params[0]; //(it's a Palina) else if(lastSearchedBusStop!=null) stopID = lastSearchedBusStop.ID; //(it's a Palina) else { publishProgress(Fetcher.Result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times try { if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) continue; } catch (NumberFormatException ex){ Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); } p= f.ReadArrivalTimesAll(stopID,resRef); //if (res.get()!= Fetcher.Result.OK) Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+resRef.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); if(gres.get() == Fetcher.Result.OK){ p.addInfoFromRoutes(branches); Thread t = new Thread(new BranchInserter(branches, context)); t.start(); otherActivities.add(t); } else{ resRef.set(Fetcher.Result.NOT_FOUND); } //put updated values into Database } if(lastSearchedBusStop != null && resRef.get()== Fetcher.Result.OK) { // check that we don't have the same stop if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } } p.mergeDuplicateRoutes(0); if (resRef.get() == Fetcher.Result.OK && p.getTotalNumberOfPassages() == 0 ) { resRef.set(Fetcher.Result.EMPTY_RESULT_SET); Log.d(DEBUG_TAG, "Setting empty results"); } publishProgress(resRef.get()); //TODO: find a way to avoid overloading the user with toasts if (resultPalina == null && f instanceof MatoAPIFetcher && p.queryAllRoutes().size() > 0){ resultPalina = p; } //find if it went well results.add(resRef.get()); if(resRef.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return p; } finalResultRef.set(resRef.get()); } /* boolean emptyResults = true; for (Fetcher.Result re: results){ if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { emptyResults = false; break; } } */ //at this point, we are sure that the result has been negative failedAll=true; return resultPalina; } @Override protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.Result r : values){ //TODO: make Toast fh.showErrorMessage(r, SearchRequestType.ARRIVALS); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Palina p) { FragmentHelper fh = helperRef.get(); if(p == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; fh.createOrUpdateStopFragment( p, replaceFragment); } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } public 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 = NextGenDB.getInstance(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList branchesValues = new ArrayList<>(routesToInsert.size()*4); ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); //values[routesToInsert.indexOf(r)] = cv; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i0) { starttime = System.currentTimeMillis(); ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); } nextGenDB.close(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt index ff89619..ba40734 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt @@ -1,245 +1,245 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.content.Context import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.mato.MatoAPIFetcher import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.middleware.RecursionHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicReference class ArrivalsViewModel(application: Application): AndroidViewModel(application) { // Arrivals of palina val appContext: Context val palinaLiveData = MediatorLiveData() val sourcesLiveData = MediatorLiveData() val resultLiveData = MutableLiveData() val currentFetchers = MediatorLiveData>() /// OLD REPO for stops instance private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) private var stopIdRequested = "" private val stopFromDB = MutableLiveData() val arrivalsRequestRunningLiveData = MutableLiveData(false) private val oldRepoStopCallback = OldDataRepository.Callback>{ stopListRes -> if(stopIdRequested.isEmpty()) return@Callback if(stopListRes.isSuccess) { val stopF = stopListRes.result!!.filter { s -> s.ID == stopIdRequested } if (stopF.isEmpty()) { Log.w(DEBUG_TAG, "Requested stop $stopIdRequested but is not in the list from database: ${stopListRes.result}") } else{ stopFromDB.postValue(stopF[0]) Log.d(DEBUG_TAG, "Setting new stop ${stopF[0]} from database") } } else{ Log.e(DEBUG_TAG, "Requested stop ${stopIdRequested} from database but error occured: ${stopListRes.exception}") } } init { appContext = application.applicationContext palinaLiveData.addSource(stopFromDB){ s -> val hasSource = palinaLiveData.value?.passaggiSourceIfAny Log.d(DEBUG_TAG, "Have current palina ${palinaLiveData.value!=null}, source passaggi $hasSource, new incoming stop $s from database") val newp = if(palinaLiveData.value == null) Palina(s) else Palina.mergePaline(palinaLiveData.value, Palina(s)) Log.d(DEBUG_TAG, "Merged palina: $newp, num passages: ${newp?.totalNumberOfPassages}, has coords: ${newp?.hasCoords()}") newp?.let { pal -> palinaLiveData.postValue(pal) } } } fun clearPalinaArrivals(palina: Palina) : Palina{ palina.clearRoutes() return palina } fun requestArrivalsForStop(stopId: String, fetchers: List){ val context = appContext //application.applicationContext currentFetchers.value = fetchers //THIS IS TOTALLY WRONG!!! /*palinaLiveData.value?.let{ palinaLiveData.value = clearPalinaArrivals(it) } */ //request stop from the DB stopIdRequested = stopId oldRepo.requestStopsWithGtfsIDs(listOf("gtt:$stopId"), oldRepoStopCallback) arrivalsRequestRunningLiveData.value = true viewModelScope.launch(Dispatchers.IO){ runArrivalsFetching(stopId, fetchers, context) } } fun requestArrivalsForStop(stopId: String, fetchersSources: Array){ val fetchers = constructFetchersFromStrList(fetchersSources) requestArrivalsForStop(stopId, fetchers) } private suspend fun runArrivalsFetching(stopId: String, fetchers: List, appContext: Context) { if (fetchers.isEmpty()) { //do nothing arrivalsRequestRunningLiveData.postValue(false) return } // Equivalente del doInBackground nell'AsyncTask val recursionHelper = RecursionHelper(fetchers.toTypedArray()) var resultPalina : Palina? = null val stringBuilder = StringBuilder() for (f in fetchers) { stringBuilder.append("") stringBuilder.append(f.javaClass.simpleName) stringBuilder.append("; ") } Log.d(DEBUG_TAG, "Using fetchers: $stringBuilder") val resultRef = AtomicReference() while (recursionHelper.valid()) { val fetcher = recursionHelper.getAndMoveForward() sourcesLiveData.postValue(fetcher.sourceForFetcher) - if (fetcher is MatoAPIFetcher) { - fetcher.appContext = appContext + if (fetcher is ArrivalsFetcherContext) { + fetcher.setContext(appContext) } Log.d(DEBUG_TAG, "Using the ArrivalsFetcher: ${fetcher.javaClass}") // Verifica se è un fetcher per MetroStop da saltare try { if (fetcher is FiveTAPIFetcher && stopId.toInt() >= 8200) { continue } } catch (ex: NumberFormatException) { Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures") } // Legge i tempi di arrivo val palina = fetcher.ReadArrivalTimesAll(stopId, resultRef) Log.d(DEBUG_TAG, "Arrivals fetcher: $fetcher\n\tProgress: ${resultRef.get()}") // Gestione del FiveTAPIFetcher per ottenere le direzioni if (fetcher is FiveTAPIFetcher) { val branchResultRef = AtomicReference() val branches = fetcher.getDirectionsForStop(stopId, branchResultRef) Log.d(DEBUG_TAG, "FiveTArrivals fetcher: $fetcher\n\tDetails req: ${branchResultRef.get()}") if (branchResultRef.get() == Fetcher.Result.OK) { palina.addInfoFromRoutes(branches) // Inserisce i dati nel database viewModelScope.launch(Dispatchers.IO) { //modify the DB in another coroutine in the background NextGenDB.insertBranchesIntoDB(appContext,branches) } } else { resultRef.set(Fetcher.Result.NOT_FOUND) } } // Unisce percorsi duplicati palina.mergeDuplicateRoutes(0) if (resultRef.get() == Fetcher.Result.OK && palina.getTotalNumberOfPassages() == 0) { resultRef.set(Fetcher.Result.EMPTY_RESULT_SET) Log.d(DEBUG_TAG, "Setting empty results") } //reportProgress resultLiveData.postValue(resultRef.get()) // Se è un MatoAPIFetcher con risultati validi, salviamo i dati if (resultPalina == null && fetcher is MatoAPIFetcher && palina.queryAllRoutes().size > 0) { resultPalina = palina } // Se abbiamo un risultato OK, restituiamo la palina if (resultRef.get() == Fetcher.Result.OK) { setResultAndPalinaFromFetchers(palina, Fetcher.Result.OK) return } //end Fetchers loop } // Se arriviamo qui, tutti i fetcher hanno fallito //failedAll = true // Se abbiamo comunque una palina, la restituiamo resultPalina?.let { setResultAndPalinaFromFetchers(it, resultRef.get()) } //in ogni caso, settiamo la richiesta come conclusa arrivalsRequestRunningLiveData.postValue(false) Log.d(DEBUG_TAG, "Finished fetchers available to search arrivals for palina stop $stopId") } private fun setResultAndPalinaFromFetchers(palina: Palina, fetcherResult: Fetcher.Result) { arrivalsRequestRunningLiveData.postValue(false) resultLiveData.postValue(fetcherResult) Log.d(DEBUG_TAG, "Have new result palina for stop ${palina.ID}, source ${palina.passaggiSourceIfAny} has coords: ${palina.hasCoords()}") Log.d(DEBUG_TAG, "Old palina liveData is: ${palinaLiveData.value?.stopDisplayName}, has Coords ${palinaLiveData.value?.hasCoords()}") palinaLiveData.postValue(Palina.mergePaline(palina, palinaLiveData.value)) } companion object{ const val DEBUG_TAG="BusTO-ArrivalsViMo" @JvmStatic fun getFetcherFromStrSource(src:String): ArrivalsFetcher?{ val srcEnum = Passaggio.Source.valueOf(src) val fe: ArrivalsFetcher? = when(srcEnum){ Passaggio.Source.FiveTAPI -> FiveTAPIFetcher() Passaggio.Source.GTTJSON -> GTTJSONFetcher() Passaggio.Source.FiveTScraper -> FiveTScraperFetcher() Passaggio.Source.MatoAPI -> MatoAPIFetcher() Passaggio.Source.UNDETERMINED -> null null -> null } return fe } @JvmStatic fun constructFetchersFromStrList(sources: Array): List{ val fetchers = mutableListOf() for(s in sources){ val fe = getFetcherFromStrSource(s) if(fe!=null){ fetchers.add(fe) } else{ Log.d(DEBUG_TAG, "Cannot convert fetcher source $s to a fetcher") } } return fetchers } } } \ No newline at end of file