commit 29fa39912fc1b8421aae6947e2b480cd061ee720 Author: Fabio Mazza Date: Mon Apr 27 13:54:38 2026 +0200 Rewrite GTT arrivals parser to use Volley Summary: Fix T1426 Test Plan: Check that the arrivals time work with GTT. Reviewers: #libre_busto_hackers, valerio.bozzolan Reviewed By: #libre_busto_hackers, valerio.bozzolan Subscribers: valerio.bozzolan Project Tags: #libre_busto Maniphest Tasks: T1426 Differential Revision: https://gitpull.it/D232 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,6 +1,7 @@ /* 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 @@ -22,6 +23,10 @@ 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; @@ -29,21 +34,19 @@ 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"); @@ -51,11 +54,9 @@ public class GTTJSONFetcher implements ArrivalsFetcher { 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; @@ -71,66 +72,154 @@ public class GTTJSONFetcher implements ArrivalsFetcher { 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 @@ -39,11 +39,8 @@ 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) 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 @@ -101,8 +101,8 @@ public class AsyncArrivalsSearcher extends AsyncTask 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()); 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 @@ -125,8 +125,8 @@ class ArrivalsViewModel(application: Application): AndroidViewModel(application) 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}")