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