diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java index 8b5c807..66466a3 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java @@ -1,533 +1,548 @@ /* 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.os.Parcel; import android.os.Parcelable; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.List; import it.reyboz.bustorino.util.LinesNameSorter; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop implements Parcelable { - private ArrayList routes = new ArrayList<>(); + private ArrayList routes = new ArrayList<>(); // the routes with arrival times private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, - s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), null); + s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), s.gtfsID); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { super(ID, name, userName, location, null, null, lat, lon, gtfsID); } public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { super(name, ID, location, type, routesThatStopHere); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; buildRoutesString(); return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } + /** + * Remove all arrivals from this Palina + */ + public void clearRoutes(){ + routes.clear(); + } + + /** + * Check how many routes (from arrival times) we have + * @return the number of routes + */ + public int getNumRoutesWithArrivals(){ + return routes.size(); + } + @Nullable @Override protected String buildRoutesString() { // no routes => no string if(routes == null || routes.size() == 0) { return ""; } /*final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); int i, lenMinusOne = routes.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routes.get(i).getName().trim()).append(", "); } // last one: sb.append(routes.get(i).getName()); */ ArrayList names = new ArrayList<>(); for (Route r: routes){ names.add(r.getName()); } final String routesThatStopHere = buildRoutesStringFromNames(names); setRoutesThatStopHereString(routesThatStopHere); return routesThatStopHereToString(); } /** * Sort the names of the routes for the string "routes stopping here" and make the string * @param names of the Routes that pass in the stop * @return the full string of routes stopping (eg, "10, 13, 42" ecc) */ public static String buildRoutesStringFromNames(List names){ final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(names, nameSorter); int i, lenMinusOne = names.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(names.get(i).trim()).append(", "); } //last one sb.append(names.get(i).trim()); return sb.toString(); } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i0) min = Math.min(min,r.numPassaggi()); } if (min == Integer.MAX_VALUE) return 0; else return min; } public ArrayList getRoutesNamesWithNoPassages(){ ArrayList mList = new ArrayList<>(); if(routes==null || routes.size() == 0){ return mList; } for(Route r: routes){ if(r.numPassaggi()==0) mList.add(r.getDisplayCode()); } return mList; } private static String pick(String a, String b) { return (a != null && !a.isEmpty()) ? a : b; } /** * Merge two Palinas, including information from both * @param p1 the first one, which has priority * @param p2 the second one * @return the merged Palina data */ public static @Nullable Palina mergePaline(@Nullable Palina p1, @Nullable Palina p2) { if (p1 == null) return p2; if (p2 == null) return p1; // --- Campi base (Stop) --- String id = p1.ID; // assumiamo stesso ID String name = pick(p1.getStopDefaultName(), p2.getStopDefaultName()); String userName = pick(p1.getStopUserName(), p2.getStopUserName()); String location = pick(p1.location, p2.location); Double lat = p1.getLatitude() != null ? p1.getLatitude() : p2.getLatitude(); Double lon = p1.getLongitude() != null ? p1.getLongitude() : p2.getLongitude(); String gtfsID = pick(p1.gtfsID, p2.gtfsID); Palina result = new Palina(id, name, userName, location, lat, lon, gtfsID); // --- Routes --- List mergedRoutes = new ArrayList<>(); boolean addFromSecond = false; if (p1.queryAllRoutes() != null) mergedRoutes.addAll(p1.routes); else if (p2.queryAllRoutes() != null) mergedRoutes.addAll(p2.routes); else { //assume the first one has more important imformation mergedRoutes.addAll(p1.routes); addFromSecond = true; } result.setRoutes(mergedRoutes); if(addFromSecond){ result.addInfoFromRoutes(p2.routes); } // Unisci eventuali duplicati (stesso routeID) result.mergeDuplicateRoutes(0); // Aggiorna stringa routes result.buildRoutesString(); return result; } /// ------- Parcelable stuff --- protected Palina(Parcel in) { super(in); routes = in.createTypedArrayList(Route.CREATOR); routesModified = in.readByte() != 0; allSource = in.readByte() == 0 ? null : Passaggio.Source.valueOf(in.readString()); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeTypedList(routes); dest.writeByte((byte) (routesModified ? 1 : 0)); if (allSource == null) { dest.writeByte((byte) 0); } else { dest.writeByte((byte) 1); dest.writeString(allSource.name()); } } public static final Creator CREATOR = new Creator() { @Override public Palina createFromParcel(Parcel in) { return new Palina(in); } @Override public Palina[] newArray(int size) { return new Palina[size]; } }; @Override public int describeContents() { return 0; } // Methods using the parcelable public byte[] asByteArray(){ final Parcel p = Parcel.obtain(); writeToParcel(p,0); final byte[] b = p.marshall(); p.recycle(); return b; } public static Palina fromByteArray(byte[] data){ final Parcel p = Parcel.obtain(); p.unmarshall(data, 0, data.length); p.setDataPosition(0); final Palina palina = Palina.CREATOR.createFromParcel(p); p.recycle(); return palina; } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/app/src/main/java/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java index b54f482..1c1477e 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -1,153 +1,142 @@ /* 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.util.Log; import androidx.annotation.Nullable; import com.android.volley.AuthFailureError; import com.android.volley.NetworkResponse; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.HttpHeaderParser; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Date; import java.util.concurrent.atomic.AtomicReference; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; public class MapiArrivalRequest extends MapiVolleyRequest { private final String stopName; private final Date startingTime; private final int timeRange, numberOfDepartures; private final AtomicReference reqRes; private final String DEBUG_TAG = "BusTO-MAPIArrivalReq"; public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, int numberOfDepartures, AtomicReference res, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { super(MatoQueries.QueryType.ARRIVALS, listener, errorListener); this.stopName = stopName; this.startingTime = startingTime; this.timeRange = timeRange; this.numberOfDepartures = numberOfDepartures; this.reqRes = res; } public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, int numberOfDepartures, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { this(stopName, startingTime, timeRange, numberOfDepartures, new AtomicReference<>(), listener, errorListener); } @Nullable @Override public byte[] getBody() throws AuthFailureError { JSONObject variables = new JSONObject(); JSONObject data = new JSONObject(); try { data.put("operationName","AllStopsDirect"); variables.put("name", stopName); variables.put("startTime", (long) startingTime.getTime()/1000); variables.put("timeRange", timeRange); variables.put("numberOfDepartures", numberOfDepartures); data.put("variables", variables); data.put("query", MatoQueries.QUERY_ARRIVALS); } catch (JSONException e) { e.printStackTrace(); throw new AuthFailureError("Error with JSON enconding",e); } String requestBody = data.toString(); - Log.d(DEBUG_TAG, "Request variables: "+ variables); + Log.d(DEBUG_TAG, "MaTO arrivals request variables: "+ variables); return requestBody.getBytes(); } @Override protected Response parseNetworkResponse(NetworkResponse response) { if(response.statusCode != 200) { reqRes.set(Fetcher.Result.SERVER_ERROR); return Response.error(new VolleyError("Response Error Code " + response.statusCode)); } final String stringResponse = new String(response.data); Palina p = null; try { JSONObject data = new JSONObject(stringResponse).getJSONObject("data"); JSONArray allStopsFound = data.getJSONArray("stops"); boolean stopFound = false; for (int i=0; i. */ 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.BuildConfig 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 } constructor(): this(DEF_MIN_NUMPASSAGGI) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val now = Calendar.getInstance().time - var numMinutes = 30 + var numMinutes = 60 var palina = Palina(stopID) - var numPassaggi = 0 var trials = 0 val numDepartures = 8 - while (numPassaggi < minNumPassaggi && trials < 2) { + var moreTime = false + var palinaOK = false + while (trials <20 && !palinaOK) { //numDepartures+=2 - numMinutes += 20 + 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(5, TimeUnit.SECONDS) + val palinaResult = future.get(15, TimeUnit.SECONDS) if (palinaResult!=null) { - /*if (BuildConfig.DEBUG) - for (r in palinaResult.queryAllRoutes()){ - Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) - }*/ + palina = palinaResult - numPassaggi = palina.minNumberOfPassages + 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=2 - + 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") 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/fragments/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt index 29919c1..850352c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt @@ -1,805 +1,840 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments import android.content.Context import android.database.Cursor import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.fragment.app.viewModels import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.PalinaAdapter import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener import it.reyboz.bustorino.backend.Passaggio.Source import it.reyboz.bustorino.data.AppDataProvider import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.UserDB import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.viewmodels.ArrivalsViewModel import java.util.* class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks { private var DEBUG_TAG = DEBUG_TAG_ALL private lateinit var stopID: String //private set private var stopName: String? = null private var prefs: DBStatusManager? = null private var listener: OnDBUpdateStatusChangeListener? = null private var justCreated = false private var lastUpdatedPalina: Palina? = null private var needUpdateOnAttach = false private var fetchersChangeRequestPending = false private var stopIsInFavorites = false //Views protected lateinit var addToFavorites: ImageButton protected lateinit var openInMapButton: ImageButton - protected lateinit var timesSourceTextView: TextView + protected lateinit var arrivalsSourceTextView: TextView private lateinit var messageTextView: TextView private lateinit var preMessageTextView: TextView // this hold the "Arrivals at: " text protected lateinit var arrivalsRecyclerView: RecyclerView private var mListAdapter: PalinaAdapter? = null private lateinit var resultsLayout : LinearLayout private lateinit var loadingMessageTextView: TextView private lateinit var progressBar: ProgressBar private lateinit var howDoesItWorkTextView: TextView private lateinit var hideHintButton: Button //private NestedScrollView theScrollView; protected lateinit var noArrivalsRecyclerView: RecyclerView private var noArrivalsAdapter: RouteOnlyLineAdapter? = null private var noArrivalsTitleView: TextView? = null private var layoutManager: GridLayoutManager? = null //private View canaryEndView; private var fetchers: List = ArrayList() private val arrivalsViewModel : ArrivalsViewModel by viewModels() private var reloadOnResume = true fun getStopID() = stopID private val palinaClickListener: PalinaClickListener = object : PalinaClickListener { override fun showRouteFullDirection(route: Route) { var routeName = route.routeLongDisplayName Log.d(DEBUG_TAG, "Make toast for line " + route.name) if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null") else if (route.destinazione == null || route.destinazione.length == 0) { Toast.makeText( context, getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT ).show() } } override fun requestShowingRoute(route: Route) { Log.d( DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}""" ) if (route.gtfsId != null) { mListener.openLineFromStop(route.gtfsId, stopID) } else { val gtfsID = FiveTNormalizer.getGtfsRouteID(route) Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID") mListener.openLineFromStop(gtfsID, stopID) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) stopID = requireArguments().getString(KEY_STOP_ID) ?: "" DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID //this might really be null stopName = requireArguments().getString(KEY_STOP_NAME) val arrivalsFragment = this listener = object : OnDBUpdateStatusChangeListener { override fun onDBStatusChanged(updating: Boolean) { if (!updating) { loaderManager.restartLoader( loaderFavId, arguments, arrivalsFragment ) } else { val lm = loaderManager lm.destroyLoader(loaderFavId) lm.destroyLoader(loaderStopId) } } override fun defaultStatusValue(): Boolean { return true } } prefs = DBStatusManager(requireContext().applicationContext, listener) justCreated = true } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.fragment_arrivals, container, false) messageTextView = root.findViewById(R.id.messageTextView) preMessageTextView = root.findViewById(R.id.arrivalsTextView) addToFavorites = root.findViewById(R.id.addToFavorites) openInMapButton = root.findViewById(R.id.openInMapButton) // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView) hideHintButton = root.findViewById(R.id.hideHintButton) //TODO: Hide this layout at the beginning, show it later resultsLayout = root.findViewById(R.id.resultsLayout) loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView) progressBar = root.findViewById(R.id.circularProgressBar) hideHintButton.setOnClickListener { v: View? -> this.onHideHint(v) } //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView) val manager = LinearLayoutManager(context) arrivalsRecyclerView.setLayoutManager(manager) val mDividerItemDecoration = DividerItemDecoration( arrivalsRecyclerView.context, manager.orientation ) arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration) - timesSourceTextView = root.findViewById(R.id.timesSourceTextView) - timesSourceTextView.setOnLongClickListener { view: View? -> + arrivalsSourceTextView = root.findViewById(R.id.timesSourceTextView) + arrivalsSourceTextView.setOnLongClickListener { view: View? -> if (!fetchersChangeRequestPending) { rotateFetchers() //Show we are changing provider - timesSourceTextView.setText(R.string.arrival_source_changing) + arrivalsSourceTextView.setText(R.string.arrival_source_changing) - //mListener.requestArrivalsForStopID(stopID) requestArrivalsForTheFragment() fetchersChangeRequestPending = true return@setOnLongClickListener true } false } - timesSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> + arrivalsSourceTextView.setOnClickListener(View.OnClickListener { view: View? -> Toast.makeText( context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT ) .show() }) //Button addToFavorites.setClickable(true) addToFavorites.setOnClickListener(View.OnClickListener { v: View? -> // add/remove the stop in the favorites toggleLastStopToFavorites() }) val displayName = requireArguments().getString(STOP_TITLE) if (displayName != null) setTextViewMessage( String.format( getString(R.string.passages_fill), displayName ) ) val probablemessage = requireArguments().getString(MESSAGE_TEXT_VIEW) if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage) messageTextView.setVisibility(View.VISIBLE) } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView) layoutManager = GridLayoutManager(context, 60) layoutManager!!.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return 12 } } noArrivalsRecyclerView.setLayoutManager(layoutManager) noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView) //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times - lastUpdatedPalina?.let { showArrivalsSources(it) } - /*if (lastUpdatedPalina?.queryAllRoutes() != null && lastUpdatedPalina!!.queryAllRoutes()!!.size >0){ - showArrivalsSources(lastUpdatedPalina!!) - } else{ - Log.d(DEBUG_TAG, "No routes names") + if(lastUpdatedPalina == null && arrivalsViewModel.palinaLiveData.value != null) { + //this updates lastUpdatedPalina and also shows the arrival source + updateFragmentData(arrivalsViewModel.palinaLiveData.value!!) + } + //lastUpdatedPalina?.let { showArrivalsSources(it) } - */ + arrivalsViewModel.arrivalsRequestRunningLiveData.observe(viewLifecycleOwner, { running -> + //UI CHANGES TO APPLY WHEN THE REQUEST IS RUNNING + mListener.toggleSpinner(running) + if(running){ + //different way of setting this flag + if(lastUpdatedPalina == null || lastUpdatedPalina?.totalNumberOfPassages==0) { + showLoadingMessageForFirstTime() + } + } else{ + //stopped running, we can show the palina + //val uname = lastUpdatedPalina?.stopDisplayName + if (lastUpdatedPalina == null || lastUpdatedPalina?.numRoutesWithArrivals == 0) { + //no passages and result is not valid + setUIForNoStopFound() + } + } + }) arrivalsViewModel.palinaLiveData.observe(viewLifecycleOwner){ - mListener.toggleSpinner(false) - Log.d(DEBUG_TAG, "New result palina observed, has coords: ${it.hasCoords()}") - if(arrivalsViewModel.resultLiveData.value==Fetcher.Result.OK){ - //the result is true - changeUIFirstSearchActive(false) + Log.d(DEBUG_TAG, "New result palina observed, has coords: ${it.hasCoords()}, title ${it?.stopDisplayName}, number of passages: ${it.totalNumberOfPassages}") + + val palinaIsValid = it!=null && (it.totalNumberOfPassages>0 || it.stopDisplayName!=null) + if (palinaIsValid){ updateFragmentData(it) - } else{ - progressBar.visibility=View.INVISIBLE - // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times - if (stopName==null) - loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) - else - loadingMessageTextView.text = getString(R.string.no_arrivals_stop) + } + if(arrivalsViewModel.arrivalsRequestRunningLiveData.value ==false) { + //finished loading + if (palinaIsValid) { + //the result is true + hideLoadingMessageAndShowResults() + } else { + setUIForNoStopFound() + } } } - + // this is only for the progress arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){ Log.d(DEBUG_TAG, "Using arrivals source: $it") val srcString = getDisplayArrivalsSource(it,requireContext()) loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString) } arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res -> + val src = arrivalsViewModel.sourcesLiveData.value when (res) { Fetcher.Result.OK -> {} - Fetcher.Result.CLIENT_OFFLINE -> showToastMessage(R.string.network_error, true) + Fetcher.Result.CLIENT_OFFLINE -> showFetcherMessage(R.string.network_error, src) Fetcher.Result.SERVER_ERROR -> { if (utils.isConnected(context)) { - showToastMessage(R.string.parsing_error, true) + showFetcherMessage(R.string.parsing_error, src) } else { - showToastMessage(R.string.network_error, true) + showFetcherMessage(R.string.network_error, src) } - showToastMessage(R.string.internal_error,true) + showFetcherMessage(R.string.internal_error,src) } - Fetcher.Result.PARSER_ERROR -> showShortToast(R.string.internal_error) - Fetcher.Result.QUERY_TOO_SHORT -> showShortToast(R.string.query_too_short) - Fetcher.Result.EMPTY_RESULT_SET -> showShortToast(R.string.no_arrivals_stop) + Fetcher.Result.PARSER_ERROR -> showFetcherMessage(R.string.internal_error, src) + Fetcher.Result.QUERY_TOO_SHORT -> showFetcherMessage(R.string.query_too_short, src) + Fetcher.Result.EMPTY_RESULT_SET -> showFetcherMessage(R.string.no_arrivals_stop, src) - Fetcher.Result.NOT_FOUND -> showShortToast(R.string.no_bus_stop_have_this_name) - else -> showShortToast(R.string.internal_error) + Fetcher.Result.NOT_FOUND -> showFetcherMessage(R.string.no_bus_stop_have_this_name, src) + else -> showFetcherMessage(R.string.internal_error, src) } } return root } private fun showShortToast(id: Int) = showToastMessage(id,true) + private fun showFetcherMessage(id: Int, source: Source?){ + val srcString = source?.let{ getDisplayArrivalsSource(it,requireContext())} + if (srcString!=null){ + Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show() + } else{ + val message = getString(id) - private fun changeUIFirstSearchActive(yes: Boolean){ + Toast.makeText(requireContext(), "$srcString : $message", Toast.LENGTH_SHORT).show() + } + } + + /*private fun changeUIFirstSearchActive(yes: Boolean){ if(yes){ resultsLayout.visibility = View.GONE progressBar.visibility = View.VISIBLE loadingMessageTextView.visibility = View.VISIBLE } else{ resultsLayout.visibility = View.VISIBLE progressBar.visibility = View.GONE loadingMessageTextView.visibility = View.GONE } } + */ + private fun showLoadingMessageForFirstTime(){ + resultsLayout.visibility = View.GONE + progressBar.visibility = View.VISIBLE + loadingMessageTextView.visibility = View.VISIBLE + } + private fun hideLoadingMessageAndShowResults(){ + resultsLayout.visibility = View.VISIBLE + progressBar.visibility = View.GONE + loadingMessageTextView.visibility = View.GONE + } + + private fun setUIForNoStopFound(){ + progressBar.visibility=View.INVISIBLE + // Avoid showing this ugly message if we have found the stop, clearly it exists but GTT doesn't provide arrival times + if (stopName==null) + loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name) + else + loadingMessageTextView.text = getString(R.string.no_arrivals_stop) + } + override fun onResume() { super.onResume() val loaderManager = loaderManager Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina") - /*if(needUpdateOnAttach){ - updateFragmentData(null); - needUpdateOnAttach=false; - }*/ - /*if(lastUpdatedPalina!=null){ - updateFragmentData(null); - showArrivalsSources(lastUpdatedPalina); - }*/ + mListener.readyGUIfor(FragmentKind.ARRIVALS) //fix bug when the list adapter is null mListAdapter?.let { resetListAdapter(it) } if (noArrivalsAdapter != null) { noArrivalsRecyclerView.adapter = noArrivalsAdapter } if (stopID.isNotEmpty()) { if (!justCreated) { fetchers = utils.getDefaultArrivalsFetchers(context) adjustFetchersToSource() if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID) } else { //start first search requestArrivalsForTheFragment() - changeUIFirstSearchActive(true) + showLoadingMessageForFirstTime() justCreated = false } //start the loader if (prefs!!.isDBUpdating(true)) { prefs!!.registerListener() } else { Log.d(DEBUG_TAG, "Restarting loader for stop") loaderManager.restartLoader( loaderFavId, arguments, this ) } updateMessage() } if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) { showHints() } } override fun onStart() { super.onStart() if (needUpdateOnAttach) { updateFragmentData(null) needUpdateOnAttach = false } } override fun onPause() { if (listener != null) prefs!!.unregisterListener() super.onPause() val loaderManager = loaderManager Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders()) loaderManager.destroyLoader(loaderFavId) } override fun onAttach(context: Context) { super.onAttach(context) //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context) } fun reloadsOnResume(): Boolean { return reloadOnResume } fun setReloadOnResume(reloadOnResume: Boolean) { this.reloadOnResume = reloadOnResume } // HINT "HOW TO USE" private fun showHints() { howDoesItWorkTextView.visibility = View.VISIBLE hideHintButton.visibility = View.VISIBLE //actionHelpMenuItem.setVisible(false); } private fun hideHints() { howDoesItWorkTextView.visibility = View.GONE hideHintButton.visibility = View.GONE //actionHelpMenuItem.setVisible(true); } fun onHideHint(v: View?) { hideHints() setOption(requireContext(), OPTION_SHOW_LEGEND, false) } - /*val currentFetchersAsArray: Array - get() { - val arr = arrayOfNulls(fetchers!!.size) - fetchers!!.toArray(arr) - return arr - } - - */ - fun getCurrentFetchersAsArray(): Array { val r= fetchers.toTypedArray() //?: emptyArray() return r } private fun rotateFetchers() { Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers") fetchers?.let { Collections.rotate(it, -1) } Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers") } /** * Update the UI with the new data * @param p the full Palina */ fun updateFragmentData(p: Palina?) { if (p != null) lastUpdatedPalina = p if (!isAdded) { //defer update at next show if (p == null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null") else needUpdateOnAttach = true } else { //set title if(stopName==null && p?.stopDisplayName != null){ stopName = p.stopDisplayName updateMessage() } val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true) - showArrivalsSources(lastUpdatedPalina!!) + p?.let { + //only update the sources if we have actual passaggi + if (arrivalsViewModel.arrivalsRequestRunningLiveData.value == false) + showArrivalsSources(lastUpdatedPalina!!) + } resetListAdapter(adapter) lastUpdatedPalina?.let{ pal -> openInMapButton.setOnClickListener { if (pal.hasCoords()) mListener.showMapCenteredOnStop(pal) } } val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages if (routesWithNoPassages.isEmpty()) { //hide the views if there are no empty routes noArrivalsRecyclerView.visibility = View.GONE noArrivalsTitleView!!.visibility = View.GONE } else { Collections.sort(routesWithNoPassages, LinesNameSorter()) noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null) noArrivalsRecyclerView.adapter = noArrivalsAdapter noArrivalsRecyclerView.visibility = View.VISIBLE noArrivalsTitleView!!.visibility = View.VISIBLE } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected fun showArrivalsSources(p: Palina) { val source = p.passaggiSourceIfAny val source_txt = getDisplayArrivalsSource(source, requireContext()) // val updatedFetchers = adjustFetchersToSource(source) if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work") val base_message = getString(R.string.times_source_fmt, source_txt) - timesSourceTextView.text = base_message - timesSourceTextView.visibility = View.VISIBLE + arrivalsSourceTextView.text = base_message + arrivalsSourceTextView.visibility = View.VISIBLE if (p.totalNumberOfPassages > 0) { - timesSourceTextView.visibility = View.VISIBLE + arrivalsSourceTextView.visibility = View.VISIBLE } else { - timesSourceTextView.visibility = View.INVISIBLE + arrivalsSourceTextView.visibility = View.INVISIBLE } fetchersChangeRequestPending = false } protected fun adjustFetchersToSource(source: Source?): Boolean { if (source == null) return false var count = 0 if (source != Source.UNDETERMINED) while (source != fetchers[0]!!.sourceForFetcher && count < 200) { //we need to update the fetcher that is requested rotateFetchers() count++ } return count < 200 } protected fun adjustFetchersToSource(): Boolean { if (lastUpdatedPalina == null) return false val source = lastUpdatedPalina!!.passaggiSourceIfAny return adjustFetchersToSource(source) } /** * Update the stop title in the fragment */ private fun updateMessage() { var message = "" if (stopName != null && !stopName!!.isEmpty()) { message = ("$stopID - $stopName") } else if (stopID != null) { message = stopID } else { Log.e("ArrivalsFragm$tag", "NO ID FOR THIS FRAGMENT - something went horribly wrong") } if (message.isNotEmpty()) { //setTextViewMessage(getString(R.string.passages_fill, message)) setTextViewMessage(message) } } /** * Set the message textView * @param message the whole message to write in the textView */ fun setTextViewMessage(message: String?) { messageTextView.text = message messageTextView.visibility = View.VISIBLE } override fun onCreateLoader(id: Int, p1: Bundle?): Loader { val args = arguments //if (args?.getString(KEY_STOP_ID) == null) throw val stopID = args?.getString(KEY_STOP_ID) ?: "" val builder = AppDataProvider.getUriBuilderToComplete() val cl: CursorLoader when (id) { loaderFavId -> { builder.appendPath("favorites").appendPath(stopID) cl = CursorLoader(requireContext(), builder.build(), UserDB.getFavoritesColumnNamesAsArray, null, null, null) } loaderStopId -> { builder.appendPath("stop").appendPath(stopID) cl = CursorLoader( requireContext(), builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME), null, null, null ) } else -> { cl = CursorLoader(requireContext(), builder.build(), null, null,null,null) Log.d(DEBUG_TAG, "This is probably going to crash") } } cl.setUpdateThrottle(500) return cl } override fun onLoadFinished(loader: Loader, data: Cursor) { when (loader.id) { loaderFavId -> { val colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]) if (data.count > 0) { // IT'S IN FAVORITES data.moveToFirst() val probableName = data.getString(colUserName) stopIsInFavorites = true if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop //update the message in the textview updateMessage() } else { stopIsInFavorites = false } updateStarIcon() /* if (stopName == null) { //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment$tag", "Stop wasn't in the favorites and has no name, looking in the DB") loaderManager.restartLoader( loaderStopId, arguments, this ) } 6 */ } /* loaderStopId -> if (data.count > 0) { data.moveToFirst() val index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ) if (index == -1) { Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now...") } stopName = data.getString(index) updateMessage() } else { Log.w("ArrivalsFragment$tag", "Stop is not inside the database... CLOISTER BELL") } */ } } override fun onLoaderReset(loader: Loader) { //NOTHING TO DO } protected fun resetListAdapter(adapter: PalinaAdapter) { mListAdapter = adapter arrivalsRecyclerView.adapter = adapter arrivalsRecyclerView.visibility = View.VISIBLE } fun toggleLastStopToFavorites() { val stop: Stop? = lastUpdatedPalina if (stop != null) { // toggle the status in background AsyncStopFavoriteAction( requireContext().applicationContext, AsyncStopFavoriteAction.Action.TOGGLE ) { v: Boolean -> updateStarIconFromLastBusStop(v) }.execute(stop) } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true) } } /** * Update the star "Add to favorite" icon */ fun updateStarIconFromLastBusStop(toggleDone: Boolean) { stopIsInFavorites = if (stopIsInFavorites) !toggleDone else toggleDone updateStarIcon() // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ fun updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID.isEmpty()) { addToFavorites.visibility = View.INVISIBLE } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled) } else { addToFavorites.setImageResource(R.drawable.ic_star_outline) } addToFavorites.visibility = View.VISIBLE } } override fun onDestroyView() { //arrivalsRecyclerView = null if (arguments != null) { - requireArguments().putString(SOURCES_TEXT, timesSourceTextView.text.toString()) + requireArguments().putString(SOURCES_TEXT, arrivalsSourceTextView.text.toString()) requireArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.text.toString()) } super.onDestroyView() } override fun getBaseViewForSnackBar(): View? { return null } fun isFragmentForTheSameStop(p: Palina): Boolean { return if (tag != null) tag == getFragmentTag(p) else false } /** * Request arrivals in the fragment */ fun requestArrivalsForTheFragment(){ // Run with previous fetchers - //fragment.getCurrentFetchers().toArray() - //AsyncArrivalsSearcher(, getCurrentFetchersAsArray(), context).execute(stopID) + context?.let { mListener.toggleSpinner(true) val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" } //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray()) //val workManager = WorkManager.getInstance(it) //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest) arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray()) //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID") } } companion object { private const val OPTION_SHOW_LEGEND = "show_legend" private const val KEY_STOP_ID = "stopid" private const val KEY_STOP_NAME = "stopname" private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment" private const val loaderFavId = 2 private const val loaderStopId = 1 const val STOP_TITLE: String = "messageExtra" private const val SOURCES_TEXT = "sources_textview_message" @JvmStatic @JvmOverloads fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment { val fragment = ArrivalsFragment() val args = Bundle() args.putString(KEY_STOP_ID, stopID) //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null) { args.putString(KEY_STOP_NAME, stopName) } fragment.arguments = args return fragment } @JvmStatic fun getFragmentTag(p: Palina): String { return "palina_" + p.ID } @JvmStatic fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID" @JvmStatic fun getDisplayArrivalsSource(source: Source, context: Context): String{ return when (source) { Source.GTTJSON -> context.getString(R.string.gttjsonfetcher) Source.FiveTAPI -> context.getString(R.string.fivetapifetcher) Source.FiveTScraper -> context.getString(R.string.fivetscraper) Source.MatoAPI -> context.getString(R.string.source_mato) Source.UNDETERMINED -> //Don't show the view context.getString(R.string.undetermined_source) } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index bc762d5..024d13f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,889 +1,890 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; //private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE! private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager childFragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; - List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); - ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; - arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ //String stopName = fragment.getStopID(); //new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); fragment.requestArrivalsForTheFragment(); } - } else //we create a new fragment, which is WRONG + } else { //we create a new fragment, which is WRONG + List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); + ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; + arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); + } } }; // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsFragmentRequest){ showNearbyFragmentIfPossible(); pendingNearbyStopsFragmentRequest = false; } } if(pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest =false; } }); private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfPossible(); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsFragmentRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfPossible(); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff childFragMan = getChildFragmentManager(); childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(requireContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){ Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } } else { //The Introductory Activity is about to be started, hence pause the request and show later pendingIntroRun = true; } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (locationManager == null) locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ //request position permission if needed if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } //deactivate flag pendingIntroRun = false; } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); //TODO: if we come back to this from another fragment, and the user has given again the permission // for the Location, we should show the Nearby Stops if(!suppressArrivalsReload && pendingStopID==null){ //none of the following cases are true // check if we are showing any fragment final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ //we are not showing anything if(Permissions.anyLocationPermissionsGranted(getContext())){ showNearbyFragmentIfPossible(); } } } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForArrivals() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForArrivals(); break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity mListener.openLineFromStop(routeGtfsId, stopIDFrom); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() fragment.requestArrivalsForTheFragment(); } else{ //SHOW NEW ARRIVALS FRAGMENT //new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfPossible() { if (isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } if (fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !childFragMan.isDestroyed()) { //Go ahead with the request actuallyShowNearbyStopsFragment(); pendingNearbyStopsFragmentRequest = false; } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /* * Run location requests separately and asynchronously class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt index 20d73f5..ff89619 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt @@ -1,225 +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 = 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 = Palina.mergePaline(palinaLiveData.value, Palina(s)) - newp?.let { palinaLiveData.value = it } + 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 } 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) - //TODO: Rotate the fetchers appropriately + 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 diff --git a/app/src/main/res/layout/fragment_arrivals.xml b/app/src/main/res/layout/fragment_arrivals.xml index 0feaa0e..069aaaa 100644 --- a/app/src/main/res/layout/fragment_arrivals.xml +++ b/app/src/main/res/layout/fragment_arrivals.xml @@ -1,248 +1,248 @@