diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index 44029e2..f2cc194 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,53 +1,54 @@ /* BusTO - Data 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; import android.os.Bundle; import androidx.appcompat.app.ActionBar; import it.reyboz.bustorino.fragments.LinesDetailFragment; import it.reyboz.bustorino.fragments.TestRealtimeGtfsFragment; import it.reyboz.bustorino.middleware.GeneralActivity; public class ActivityExperiments extends GeneralActivity { final static String DEBUG_TAG = "ExperimentsGTFS"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_experiments); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setIcon(R.drawable.ic_launcher); } if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) /* .add(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs("gtt:56U")) .commit(); */ - .add(R.id.fragment_container_view, TestRealtimeGtfsFragment.class, null) + .add(R.id.fragment_container_view, LinesDetailFragment.class, + LinesDetailFragment.Companion.makeArgs("gtt:10U")) .commit(); } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt index 66d8f00..a69155e 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -1,189 +1,189 @@ /* 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 class MatoQueries { companion object{ const val QUERY_ARRIVALS="""query AllStopsDirect( ${'$'}name: String ${'$'}startTime: Long ${'$'}timeRange: Int ${'$'}numberOfDepartures: Int ) { stops(name: ${'$'}name) { __typename lat lon gtfsId code name desc wheelchairBoarding routes { __typename gtfsId shortName } stoptimesForPatterns( startTime: ${'$'}startTime timeRange: ${'$'}timeRange numberOfDepartures: ${'$'}numberOfDepartures ) { __typename pattern { __typename headsign directionId route { __typename gtfsId shortName mode } } stoptimes { __typename scheduledArrival realtimeArrival realtime realtimeState } } } } """ const val ALL_STOPS_BY_FEEDS=""" query AllStops(${'$'}feeds: [String!]){ stops(feeds: ${'$'}feeds) { lat lon gtfsId code name desc routes { gtfsId shortName } } } """ const val ALL_FEEDS=""" query AllFeeds{ feeds{ feedId agencies{ gtfsId name url fareUrl phone } } } """ const val ROUTES_BY_FEED=""" query AllRoutes(${'$'}feeds: [String]){ routes(feeds: ${'$'}feeds) { agency{ gtfsId } gtfsId shortName longName type desc color textColor } } """ const val ROUTES_WITH_PATTERNS=""" query RoutesWithPatterns(${'$'}routes: [String]) { routes(ids: ${'$'}routes) { gtfsId shortName longName type patterns{ name code semanticHash directionId headsign stops{ gtfsId lat lon } patternGeometry{ length points } } } } """ const val TRIP_DETAILS=""" - query TripInfo(${'$'}field: String!){ - trip(id: ${'$'}field){ + query TripInfo(${'$'}field: String!){ + trip(id: ${'$'}field){ + gtfsId + serviceId + route{ gtfsId - serviceId - route{ - gtfsId - } - pattern{ - name - code - headsign - } - wheelchairAccessible - activeDates - tripShortName - tripHeadsign - bikesAllowed - semanticHash } + pattern{ + name + code + headsign + } + wheelchairAccessible + activeDates + tripShortName + tripHeadsign + bikesAllowed + semanticHash } + } """ fun getNameAndRequest(type: QueryType): Pair{ return when (type){ QueryType.FEEDS -> Pair("AllFeeds", ALL_FEEDS) QueryType.ALL_STOPS -> Pair("AllStops", ALL_STOPS_BY_FEEDS) QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS) QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED) QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS) QueryType.TRIP -> Pair("TripInfo", TRIP_DETAILS) } } } enum class QueryType { ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES, TRIP } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt index 7c7f80c..62e9833 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -1,153 +1,146 @@ /* BusTO - Backend components Copyright (C) 2022 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 it.reyboz.bustorino.data.gtfs.* import org.json.JSONException import org.json.JSONObject +import kotlin.jvm.Throws /** * Class to hold the code for the parsing of responses from the Mato API, * from the JSON Object */ abstract class ResponseParsing{ - companion object{ + companion object { - final val DEBUG_TAG="BusTO:MatoResponseParse" + final val DEBUG_TAG = "BusTO:MatoResponseParse" fun parseAgencyJSON(jsonObject: JSONObject): GtfsAgency { return GtfsAgency( jsonObject.getString("gtfsId"), jsonObject.getString("name"), jsonObject.getString("url"), jsonObject.getString("fareUrl"), jsonObject.getString("phone"), null ) } /** * Parse a feed request json, containing the GTFS agencies it is served by */ fun parseFeedJSON(jsonObject: JSONObject): Pair> { val agencies = ArrayList() val feed = GtfsFeed(jsonObject.getString("feedId")) val oo = jsonObject.getJSONArray("agencies") agencies.ensureCapacity(oo.length()) - for (i in 0 until oo.length()){ + for (i in 0 until oo.length()) { val agObj = oo.getJSONObject(i) agencies.add( GtfsAgency( agObj.getString("gtfsId"), agObj.getString("name"), agObj.getString("url"), agObj.getString("fareUrl"), agObj.getString("phone"), feed ) ) } return Pair(feed, agencies) } fun parseRouteJSON(jsonObject: JSONObject): GtfsRoute { val agencyJSON = jsonObject.getJSONObject("agency") val agencyId = agencyJSON.getString("gtfsId") return GtfsRoute( jsonObject.getString("gtfsId"), agencyId, jsonObject.getString("shortName"), jsonObject.getString("longName"), jsonObject.getString("desc"), GtfsMode.getByValue(jsonObject.getInt("type"))!!, jsonObject.getString("color"), jsonObject.getString("textColor") ) } /** * Parse a route pattern from the JSON response of the MaTO server */ - fun parseRoutePatternsStopsJSON(jsonObject: JSONObject) : ArrayList{ + fun parseRoutePatternsStopsJSON(jsonObject: JSONObject): ArrayList { val routeGtfsId = jsonObject.getString("gtfsId") val patternsJSON = jsonObject.getJSONArray("patterns") val patternsOut = ArrayList(patternsJSON.length()) var mPatternJSON: JSONObject - for(i in 0 until patternsJSON.length()){ + for (i in 0 until patternsJSON.length()) { mPatternJSON = patternsJSON.getJSONObject(i) val stopsJSON = mPatternJSON.getJSONArray("stops") val stopsCodes = ArrayList(stopsJSON.length()) - for(k in 0 until stopsJSON.length()){ + for (k in 0 until stopsJSON.length()) { stopsCodes.add( stopsJSON.getJSONObject(k).getString("gtfsId") ) } val geometry = mPatternJSON.getJSONObject("patternGeometry") val numGeo = geometry.getInt("length") val polyline = geometry.getString("points") patternsOut.add( MatoPattern( mPatternJSON.getString("name"), mPatternJSON.getString("code"), mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), - routeGtfsId,mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes + routeGtfsId, mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes ) ) } return patternsOut } - fun parseTripInfo(jsonData: JSONObject): GtfsTrip?{ + @Throws(JSONException::class) + fun parseTripInfo(jsonData: JSONObject): GtfsTrip { + val jsonTrip = jsonData.getJSONObject("trip") - return try { - val jsonTrip = jsonData.getJSONObject("trip") + val routeId = jsonTrip.getJSONObject("route").getString("gtfsId") - val routeId = jsonTrip.getJSONObject("route").getString("gtfsId") - - val patternId =jsonTrip.getJSONObject("pattern").getString("code") - // still have "activeDates" which are the days in which the pattern is active - //Log.d("BusTO:RequestParsing", "Making GTFS trip for: $jsonData") - val trip = GtfsTrip( - routeId, jsonTrip.getString("serviceId"), jsonTrip.getString("gtfsId"), - jsonTrip.getString("tripHeadsign"), -1, "", "", - Converters.wheelchairFromString(jsonTrip.getString("wheelchairAccessible")), - false, patternId, jsonTrip.getString("semanticHash") - ) - trip - } catch (e: JSONException){ - Log.e(DEBUG_TAG, "Cannot parse json to make trip") - Log.e(DEBUG_TAG, "Json Data: $jsonData") - e.printStackTrace() - null - } + val patternId = jsonTrip.getJSONObject("pattern").getString("code") + // still have "activeDates" which are the days in which the pattern is active + //Log.d("BusTO:RequestParsing", "Making GTFS trip for: $jsonData") + val trip = GtfsTrip( + routeId, jsonTrip.getString("serviceId"), jsonTrip.getString("gtfsId"), + jsonTrip.getString("tripHeadsign"), -1, "", "", + Converters.wheelchairFromString(jsonTrip.getString("wheelchairAccessible")), + false, patternId, jsonTrip.getString("semanticHash") + ) + return trip } - } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java index 3d669c1..5af122e 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,184 +1,188 @@ /* BusTO - Data 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.data; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Notifications; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DBUpdateWorker extends Worker{ public static final String ERROR_CODE_KEY ="Error_Code"; public static final String ERROR_REASON_KEY = "ERROR_REASON"; public static final int ERROR_FETCHING_VERSION = 4; public static final int ERROR_DOWNLOADING_STOPS = 5; public static final int ERROR_DOWNLOADING_LINES = 6; + public static final int ERROR_CODE_DB_CLOSED=-2; public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; private final static int NOTIFIC_ID =32198; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @SuppressLint("RestrictedApi") @NonNull @Override public Result doWork() { //register Notification channel final Context con = getApplicationContext(); //Notifications.createDefaultNotificationChannel(con); //Use the new notification channels Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, Notifications.DB_UPDATE_CHANNELS_ID ); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); final int notification_ID = 32198; final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); final int new_DB_version = DatabaseUpdate.getNewVersion(); final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); long currentTime = System.currentTimeMillis()/1000; //showNotification(notificationManager, notification_ID); final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, Notifications.DB_UPDATE_CHANNELS_ID) .setContentTitle(con.getString(R.string.database_update_msg_notif)) .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); notificationManager.notify(notification_ID,builder.build()); Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); /* SKIP CHECK (Reason: The Old API might fail at any moment) if (new_DB_version < 0){ //there has been an error final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) .putInt(ERROR_CODE_KEY,new_DB_version).build(); cancelNotification(notificationID); return ListenableWorker.Result.failure(out); } */ //we got a good version if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) && !isUpdateCompulsory) { //don't need to update cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } //start the real update AtomicReference resultAtomicReference = new AtomicReference<>(); DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ //Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); break; case ERROR_LINES_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; + case DB_CLOSED: + dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DB_CLOSED); + break; } cancelNotification(notification_ID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); //update the version in the shared preference final SharedPreferences.Editor editor = shPr.edit(); editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); currentTime = System.currentTimeMillis()/1000; editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } public static Constraints getWorkConstraints(){ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build(); } public static WorkRequest newFirstTimeWorkRequest(){ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) //.setInputData(new Data.Builder().putBoolean()) .build(); } /* private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, final String channel_ID){ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); notificManager.notify(notification_ID,builder.build()); return notification_ID; } */ private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.cancel(notificationID); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java index 4d27ce2..5314ccd 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,303 +1,307 @@ /* BusTO - Data 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.data; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.gtfs.GtfsAgency; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.data.gtfs.GtfsFeed; import it.reyboz.bustorino.data.gtfs.GtfsRoute; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.PatternStop; import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; enum Result { - DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD + DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD, DB_CLOSED } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } try { JSONObject resp = new JSONObject(networkRequest); return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); return JSON_PARSING_ERROR; } } private static boolean updateGTFSAgencies(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( con, res ); dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); return true; } private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final List routes= MatoAPIFetcher.Companion.getRoutes(con, res); final HashMap> routesStoppingInStop = new HashMap<>(); dao.insertRoutes(routes); if(res.get()!= Fetcher.Result.OK){ return routesStoppingInStop; } final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); final HashMap routesMap = new HashMap<>(routes.size()); for(GtfsRoute r: routes){ gtfsRoutesIDs.add(r.getGtfsId()); routesMap.put(r.getGtfsId(),r); } long t0 = System.currentTimeMillis(); final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); long tend = System.currentTimeMillis() - t0; Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); if(res.get()!=Fetcher.Result.OK){ Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); return routesStoppingInStop; } //match patterns with routes final ArrayList patternStops = new ArrayList<>(patterns.size()); for(MatoPattern p: patterns){ //scan patterns final ArrayList stopsIDs = p.getStopsGtfsIDs(); final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId()); if (mRoute == null) { Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map"); } for (int i=0; i()); } Set mset= routesStoppingInStop.get(ID); assert mset != null; mset.add(mRoute.getShortName()); } } dao.insertPatterns(patterns); dao.insertPatternStops(patternStops); return routesStoppingInStop; } /** * Run the DB Update * @param con a context * @param gres a result reference * @return result of the update */ public static Result performDBUpdate(Context con, AtomicReference gres) { - final FiveTAPIFetcher f = new FiveTAPIFetcher(); - - final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); - final SQLiteDatabase db = dbHelp.getWritableDatabase(); - - final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); - if (gres.get() != Fetcher.Result.OK) { - Log.w(DEBUG_TAG, "Something went wrong downloading"); - return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; - - } - // GTFS data fetching AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); updateGTFSAgencies(con, gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); } else{ Log.d(DEBUG_TAG, "Done downloading agencies"); } gtfsRes.set(Fetcher.Result.OK); final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes); if (gtfsRes.get()!= Fetcher.Result.OK){ Log.w(DEBUG_TAG, "Could not insert the routes into DB"); } else{ Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); } - /*db.beginTransaction(); startTime = System.currentTimeMillis(); int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop); if(countStop!= routesStoppingByStop.size()){ Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with " +routesStoppingByStop.size()+" stops to update"); } db.setTransactionSuccessful(); db.endTransaction(); endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms"); */ + // Stops insertion + final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); + if (gres.get() != Fetcher.Result.OK) { + Log.w(DEBUG_TAG, "Something went wrong downloading stops"); + return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + + } + final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); + final SQLiteDatabase db = dbHelp.getWritableDatabase(); + + if(!db.isOpen()){ + //catch errors like: java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase + //we have to abort the work and restart it + return Result.DB_CLOSED; + } //TODO: Get the type of stop from the lines //Empty the needed tables + db.beginTransaction(); //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); //db.delete(LinesTable.TABLE_NAME,null,null); //put new data long startTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); String routesStoppingString=""; int patternsStopsHits = 0; for (final Palina p : palinasMatoAPI) { final ContentValues cv = new ContentValues(); cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); if (p.location != null) cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){ final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID)); routesStoppingString = Palina.buildRoutesStringFromNames(routesSs); patternsStopsHits++; } else{ routesStoppingString = p.routesThatStopHereToString(); } cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString); if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); } db.setTransactionSuccessful(); db.endTransaction(); long endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); + db.close(); dbHelp.close(); return DatabaseUpdate.Result.DONE; } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /** * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now */ public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); final Data reqData = new Data.Builder() .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .setInputData(reqData) .build(); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10); if ((version >= 0 || lastDBUpdateTime >=0) && !restart) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); } /* public static boolean isDBUpdating(){ return false; TODO } */ public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, @NonNull Observer> observer) { WorkManager workManager = WorkManager.getInstance(context); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( lifecycleOwner, observer ); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt index da00808..ac3c597 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt @@ -1,132 +1,136 @@ /* BusTO - Data components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.lifecycle.viewModelScope import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters import com.android.volley.Response import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.Notifications import it.reyboz.bustorino.data.gtfs.GtfsTrip import kotlinx.coroutines.launch import java.util.concurrent.CountDownLatch class MatoDownloadTripsWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { //val imageUriInput = // inputData.("IMAGE_URI") ?: return Result.failure() val tripsList = inputData.getStringArray(TRIPS_KEYS) if (tripsList== null){ Log.e(DEBUG_TAG,"trips list given is null") return Result.failure() } val gtfsRepository = GtfsRepository(applicationContext) val matoRepository = MatoRepository(applicationContext) //clear the matoTrips val queriedMatoTrips = HashSet() val downloadedMatoTrips = ArrayList() val failedMatoTripsDownload = HashSet() Log.i(DEBUG_TAG, "Requesting download for the trips") val requestCountDown = CountDownLatch(tripsList.size); for(trip in tripsList){ queriedMatoTrips.add(trip) matoRepository.requestTripUpdate(trip,{error-> - Log.e(DEBUG_TAG, "Cannot download Gtfs Trip") - error.printStackTrace() + Log.e(DEBUG_TAG, "Cannot download Gtfs Trip $trip") + val stacktrace = error.stackTrace.take(5) + Log.w(DEBUG_TAG, "Stacktrace:\n$stacktrace") failedMatoTripsDownload.add(trip) requestCountDown.countDown() }){ if(it.isSuccess){ + if (it.result == null){ + Log.e(DEBUG_TAG, "Got null result"); + } downloadedMatoTrips.add(it.result!!) } else{ failedMatoTripsDownload.add(trip) } Log.i( DEBUG_TAG,"Result download, so far, trips: ${queriedMatoTrips.size}, failed: ${failedMatoTripsDownload.size}," + " succeded: ${downloadedMatoTrips.size}") //check if we can insert the trips requestCountDown.countDown() } } requestCountDown.await() val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID } val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted) Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert") if(doInsert){ gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips) } return Result.success() } override suspend fun getForegroundInfo(): ForegroundInfo { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val context = applicationContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( Notifications.DB_UPDATE_CHANNELS_ID, context.getString(R.string.database_notification_channel), NotificationManager.IMPORTANCE_MIN ) notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID) //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) .setSmallIcon(R.drawable.bus) .setOngoing(true) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setContentTitle(context.getString(R.string.app_name)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setContentText("Downloading data") .build() return ForegroundInfo(NOTIFICATION_ID, notification) } companion object{ const val TRIPS_KEYS = "tripsToDownload" const val WORK_TAG = "tripsDownloaderAndInserter" const val DEBUG_TAG="BusTO:MatoTripDownWRK" const val NOTIFICATION_ID=424242 } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoRepository.kt index e398ee6..ceecc37 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoRepository.kt @@ -1,58 +1,59 @@ /* BusTO - Data components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data import android.content.Context import android.util.Log import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.mato.MatoQueries import it.reyboz.bustorino.backend.mato.MatoVolleyJSONRequest import it.reyboz.bustorino.backend.mato.ResponseParsing import it.reyboz.bustorino.data.gtfs.GtfsTrip import org.json.JSONException import org.json.JSONObject class MatoRepository(val mContext: Context) { private val netVolleyManager = NetworkVolleyManager.getInstance(mContext) fun requestTripUpdate(tripId: String, errorListener: Response.ErrorListener?, callback: Callback){ val params = JSONObject() params.put("field",tripId) Log.i(DEBUG_TAG, "Requesting info for trip id: $tripId") netVolleyManager.addToRequestQueue(MatoVolleyJSONRequest( MatoQueries.QueryType.TRIP,params,{ try { - val result = Result.success(ResponseParsing.parseTripInfo(it)) + val trip: GtfsTrip = ResponseParsing.parseTripInfo(it) + val result = Result.success(trip) callback.onResultAvailable(result) } catch (e: JSONException){ // this might happen when the json is "{'data': {'trip': None}}" callback.onResultAvailable(Result.failure(e)) } }, errorListener )) } fun interface Callback { fun onResultAvailable(result: Result) } companion object{ final val DEBUG_TAG ="BusTO:MatoRepository" } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index 637720a..1007c3e 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,164 +1,195 @@ package it.reyboz.bustorino.fragments import android.os.Bundle import android.os.Parcelable import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Spinner import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import it.reyboz.bustorino.R import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop +import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint +import org.osmdroid.util.MapTileIndex import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Polyline class LinesDetailFragment() : Fragment() { private lateinit var lineID: String private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List private lateinit var gtfsStopsForCurrentPattern: List private lateinit var map: MapView private lateinit var viewingPattern: MatoPatternWithStops private lateinit var viewModel: LinesViewModel private var polyline = Polyline(); + private var stopPosList = ArrayList() companion object { private const val LINEID_KEY="lineID" fun newInstance() = LinesDetailFragment() const val DEBUG_TAG="LinesDetailFragment" fun makeArgs(lineID: String): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) return b } private const val DEFAULT_CENTER_LAT = 45.0708 private const val DEFAULT_CENTER_LON = 7.6858 } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter map = rootView.findViewById(R.id.lineMap) + val USGS_SAT: OnlineTileSourceBase = object : OnlineTileSourceBase( + "USGS National Map Sat", + 0, + 15, + 256, + "", + arrayOf("https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/"), + "USGS" + ) { + override fun getTileURLString(pMapTileIndex: Long): String { + return baseUrl + MapTileIndex.getZoom(pMapTileIndex) + "/" + MapTileIndex.getY(pMapTileIndex) + "/" + MapTileIndex.getX( + pMapTileIndex + ) + } + } map.setTileSource(TileSourceFactory.MAPNIK) + /* + object : OnlineTileSourceBase("USGS Topo", 0, 18, 256, "", + arrayOf("https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/" )) { + override fun getTileURLString(pMapTileIndex: Long) : String{ + return baseUrl + + MapTileIndex.getZoom(pMapTileIndex)+"/" + MapTileIndex.getY(pMapTileIndex) + + "/" + MapTileIndex.getX(pMapTileIndex)+ mImageFilenameEnding; + } + } + */ //map.setTilesScaledToDpi(true); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true) + map.setUseDataConnection(true) // add ability to zoom with 2 fingers map.setMultiTouchControls(true) - map.minZoomLevel = 14.0 + map.minZoomLevel = 10.0 //map controller setup val mapController = map.controller - mapController.setZoom(14.0) + mapController.setZoom(12.0) mapController.setCenter(GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON)) map.invalidate() viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") val pattern = viewingPattern.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) //val polyLine=Polyline(map) //polyLine.setPoints(pointsList) + //save points if(map.overlayManager.contains(polyline)){ map.overlayManager.remove(polyline) } polyline = Polyline(map) polyline.setPoints(pointsList) map.overlayManager.add(polyline) - //map.controller.animateTo(pointsList[0]) + map.controller.animateTo(pointsList[0]) + map.invalidate() } viewModel.setRouteIDQuery(lineID) Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") //listeners patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) setPatternAndReqStops(patternWithStops) } override fun onNothingSelected(p0: AdapterView<*>?) { } } return rootView } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) } private fun savePatternsToShow(patterns: List){ currentPatterns = patterns.sortedBy { p-> p.pattern.code } patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } val pos = patternsSpinner.selectedItemPosition //might be possible that the selectedItem is different (larger than list size) if(pos!= AdapterView.INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ val p = currentPatterns[pos] Log.d(LinesFragment.DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}") setPatternAndReqStops(currentPatterns[pos]) } Log.d(DEBUG_TAG, "Patterns changed") } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") gtfsStopsForCurrentPattern = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index 48c41a5..bea3d02 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,820 +1,824 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo 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.fragments; import android.Manifest; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate; import it.reyboz.bustorino.backend.gtfs.GtfsUtils; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.lang.ref.WeakReference; import java.util.*; import kotlin.Pair; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; public class MapFragment extends ScreenBaseFragment { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; //the asynctask used to get the stops from the database private AsyncStopFetcher stopFetcher = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; private boolean hasMapStartFinished = false; private boolean followingLocation = false; private MapViewModel mapViewModel ; //= new ViewModelProvider(this).get(MapViewModel.class); private final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; private final HashMap tripMarkersAnimators = new HashMap<>(); protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = root.findViewById(R.id.icon_center_map); btFollowMe = root.findViewById(R.id.icon_follow); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //setup Bus Markers Overlay busPositionsOverlay = new FolderOverlay(); //reset shown bus updates busPositionMarkersByTrip.clear(); tripMarkersAnimators.clear(); //set map not done hasMapStartFinished = false; //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.locationPermissionGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.locationPermissionGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); mapViewModel = new ViewModelProvider(this).get(MapViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; //stop animations // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); Log.w(DEBUG_TAG, "On pause called mapfrag"); saveMapState(); for (ObjectAnimator animator : tripMarkersAnimators.values()) { if(animator!=null && animator.isRunning()){ animator.cancel(); } } tripMarkersAnimators.clear(); if (stopFetcher!= null) stopFetcher.cancel(true); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); if (map == null){ //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null"); return; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); if(mapViewModel!=null) { mapViewModel.requestUpdates(); //mapViewModel.testCascade(); mapViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); mapViewModel.downloadandInsertTripsInDB(dat); }); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, ""); } //ask for location permission if(!Permissions.locationPermissionGranted(activity)){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.locationPermissionGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.locationPermissionGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true); map.getController().animateTo(marker); } //add the overlays with the bus stops if(busPositionsOverlay == null){ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = new FolderOverlay(); } if(mapViewModel!=null){ //should always be the case mapViewModel.getUpdatesWithTripAndPatterns().observe(this, data->{ Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished); if (hasMapStartFinished) updateBusPositionsInMap(data); if(!isDetached()) mapViewModel.requestDelayedUpdates(4000); }); } map.getOverlays().add(this.busPositionsOverlay); //set map as started hasMapStartFinished = true; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } private void updateBusMarker(final Marker marker,final GtfsPositionUpdate posUpdate,@Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); ObjectAnimator valueAnimator = MarkerAnimation.makeMarkerAnimator(map, marker, newpos, new GeoPointInterpolator.LinearFixed(), 2500); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { valueAnimator.setAutoCancel(true); } else if(tripMarkersAnimators.containsKey(updateID)) { ObjectAnimator otherAnim = tripMarkersAnimators.get(updateID); assert otherAnim != null; otherAnim.cancel(); } tripMarkersAnimators.put(updateID,valueAnimator); valueAnimator.start(); } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); marker.setPosition(position); } marker.setRotation(posUpdate.getBearing()*(-1.f)); } private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; final GtfsPositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)){ //need to change the position of the marker final Marker marker = busPositionMarkersByTrip.get(tripID); assert marker!=null; updateBusMarker(marker, update, false); if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow(); if(tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.getPattern()); } } } else{ //marker is not there, need to make it if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode"); final Marker marker = new Marker(map); /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( getResources(), R.drawable.point_heading_icon, R.dimen.map_icons_size, R.dimen.map_icons_size); */ String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.point_heading_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, route,12); */ assert mdraw != null; //mdraw.setBounds(0,0,28,28); marker.setIcon(mdraw); if(tripWithPatternStops == null){ - Log.d(DEBUG_TAG, "Trip "+tripID+" has no pattern"); + noPatternsTrips.add(tripID); } marker.setInfoWindow(new BusInfoWindow(map, update, tripWithPatternStops != null ? tripWithPatternStops.getPattern() : null, new BusInfoWindow.onTouchUp() { @Override public void onActionUp() { } })); updateBusMarker(marker, update, true); // the overlay is null when it's not attached yet? // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if(busPositionsOverlay!=null) { busPositionsOverlay.add(marker); busPositionMarkersByTrip.put(tripID, marker); } } } + if(noPatternsTrips.size()>0){ + Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips); + } } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return null; } /** * Simple asyncTask class to load the stops in the background * Holds a weak reference to the fragment to do callbacks */ static class AsyncStopFetcher extends AsyncTask>{ final WeakReference fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List doInBackground(BoundingBoxLimit... limits) { if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return null; } final BoundingBoxLimit limit = limits[0]; //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); NextGenDB dbHelper = NextGenDB.getInstance(fragmentWeakReference.get().getContext()); ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, limit.longFrom, limit.latitTo); dbHelper.close(); return stops; } @Override protected void onPostExecute(List stops) { super.onPostExecute(stops); //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); if(fragmentWeakReference.get()==null) { Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return; } if (stops!=null) Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); fragmentWeakReference.get().showStopsMarkers(stops); } private static class BoundingBoxLimit{ final double longFrom, longTo, latitFrom, latitTo; public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { this.longFrom = longFrom; this.longTo = longTo; this.latitFrom = latitFrom; this.latitTo = latitTo; } } } }