diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java index 4bcfb7b..47c5496 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java @@ -1,47 +1,80 @@ package it.reyboz.bustorino.backend; +import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import androidx.core.app.NotificationCompat; import it.reyboz.bustorino.R; public class Notifications { public static final String DEFAULT_CHANNEL_ID ="Default"; public static final String DB_UPDATE_CHANNELS_ID ="Database Update"; public static void createDefaultNotificationChannel(Context context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getString(R.string.default_notification_channel); String description = context.getString(R.string.default_notification_channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } /** * Register a notification channel on Android Oreo and above * @param con a Context * @param name channel name * @param description channel description * @param importance channel importance (from NotificationManager) * @param ID channel ID */ public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = con.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } + + + public static Notification makeMatoDownloadNotification(Context context,String title){ + return new 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(title) + .build(); + } + public static Notification makeMatoDownloadNotification(Context context){ + return makeMatoDownloadNotification(context, "Downloading data from MaTO"); + } + + public static void createDBNotificationChannel(Context context){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + Notifications.DB_UPDATE_CHANNELS_ID, + context.getString(R.string.database_notification_channel), + NotificationManager.IMPORTANCE_MIN + ); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index 32acc65..f870d26 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,434 +1,419 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.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 palina = Palina(stopID) var numPassaggi = 0 var trials = 0 val numDepartures = 8 while (numPassaggi < minNumPassaggi && trials < 2) { //numDepartures+=2 numMinutes += 20 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) try { val palinaResult = future.get(5, 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 } 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 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.setGtfsId(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 } 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( 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 outObj = "" try { val 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, "Downloading feeds: $outObj") } - /* - var numRequests = 0 - for(routeName in routesGTFSIds){ - if (!routeName.isEmpty()) numRequests++ - } - val countDownForRequests = CountDownLatch(numRequests) - val lockSave = ReentrantLock() - //val countDownFor - for (routeName in routesGTFSIds){ - val pars = JSONObject() - pars.put("") - } - val goodResponseListener = Response.Listener { } - val errorResponseListener = Response.ErrorListener { } - */ return patterns } } } \ 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 a69155e..2797ff1 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 } } """ - + //Query for the patterns, with the associated route 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){ gtfsId serviceId route{ gtfsId } 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/data/DatabaseUpdate.java b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java index 5314ccd..4036afe 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,307 +1,330 @@ /* 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, 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()); + final ArrayList patternStops = makeStopsForPatterns(patterns); + final List allPatternsCodeInDB = dao.getPatternsCodes(); + final HashSet patternsCodesToDelete = new HashSet<>(allPatternsCodeInDB); 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()); + if (!routesStoppingInStop.containsKey(sID)) { + routesStoppingInStop.put(sID, new HashSet<>()); } - Set mset= routesStoppingInStop.get(ID); + Set mset = routesStoppingInStop.get(sID); assert mset != null; mset.add(mRoute.getShortName()); } + //finally, remove from deletion list + patternsCodesToDelete.remove(p.getCode()); } + // final time for insert dao.insertPatterns(patterns); + // clear patterns that are unused + Log.d(DEBUG_TAG, "Have to remove "+patternsCodesToDelete.size()+ " patterns from the DB"); + dao.deletePatternsWithCodes(new ArrayList<>(patternsCodesToDelete)); dao.insertPatternStops(patternStops); return routesStoppingInStop; } + /** + * Make the list of stops that each pattern does, to be inserted into the DB + * @param patterns the MatoPattern + * @return a list of PatternStop + */ + public static ArrayList makeStopsForPatterns(List patterns){ + final ArrayList patternStops = new ArrayList<>(patterns.size()); + for (MatoPattern p: patterns){ + final ArrayList stopsIDs = p.getStopsGtfsIDs(); + for (int i=0; i gres) { // 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/MatoPatternsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt new file mode 100644 index 0000000..e263464 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoPatternsDownloadWorker.kt @@ -0,0 +1,101 @@ +/* + 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.NotificationManager +import android.content.Context +import android.util.Log +import androidx.work.* +import it.reyboz.bustorino.backend.Fetcher +import it.reyboz.bustorino.backend.Notifications +import it.reyboz.bustorino.backend.mato.MatoAPIFetcher +import java.util.concurrent.atomic.AtomicReference + +class MatoPatternsDownloadWorker(appContext: Context, workerParams: WorkerParameters) + : CoroutineWorker(appContext, workerParams) { + + + override suspend fun doWork(): Result { + val routesList = inputData.getStringArray(ROUTES_KEYS) + + if (routesList== null){ + Log.e(DEBUG_TAG,"routes list given is null") + return Result.failure() + } + + val res = AtomicReference(Fetcher.Result.OK) + + val gtfsRepository = GtfsRepository(applicationContext) + val patterns = MatoAPIFetcher.getPatternsWithStops(applicationContext, routesList.asList().toMutableList(), res) + if (res.get() != Fetcher.Result.OK) { + Log.e(DatabaseUpdate.DEBUG_TAG, "Something went wrong downloading patterns") + return Result.failure() + } + + gtfsRepository.gtfsDao.insertPatterns(patterns) + //Insert the PatternStops + gtfsRepository.gtfsDao.insertPatternStops(DatabaseUpdate.makeStopsForPatterns(patterns)) + return Result.success() + } + + + override suspend fun getForegroundInfo(): ForegroundInfo { + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val context = applicationContext + Notifications.createDBNotificationChannel(context) + + return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context)) + } + + + companion object{ + const val ROUTES_KEYS = "routesToDownload" + const val DEBUG_TAG="BusTO:MatoPattrnDownWRK" + const val NOTIFICATION_ID=21983102 + + const val TAG_PATTERNS ="matoPatternsDownload" + + + + fun downloadPatternsForRoutes(routesIds: List, context: Context): Boolean{ + if(routesIds.isEmpty()) return false; + + val workManager = WorkManager.getInstance(context); + val info = workManager.getWorkInfosForUniqueWork(TAG_PATTERNS).get() + + val runNewWork = if(info.isEmpty()) true + else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED + val addDat = if(info.isEmpty()) + null else info[0].state + + Log.d(DEBUG_TAG, "Request to download and insert patterns for ${routesIds.size} routes, proceed: $runNewWork, workstate: $addDat") + if(runNewWork){ + val routeIdsArray = routesIds.toTypedArray() + val dataBuilder = Data.Builder().putStringArray(ROUTES_KEYS,routeIdsArray) + + val requ = OneTimeWorkRequest.Builder(MatoPatternsDownloadWorker::class.java) + .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(TAG_PATTERNS) + .build() + workManager.enqueueUniqueWork(TAG_PATTERNS, ExistingWorkPolicy.KEEP, requ) + } + return true + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt similarity index 65% rename from app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt rename to app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt index ac3c597..0a9502e 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt @@ -1,136 +1,135 @@ /* 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 androidx.work.* 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) +class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { - //val imageUriInput = - // inputData.("IMAGE_URI") ?: return Result.failure() + return downloadGtfsTrips() + } + + /** + * Download GTFS Trips from Mato + */ + private fun downloadGtfsTrips():Result{ 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 $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}") + " 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) - } + Notifications.createDBNotificationChannel(context) - 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) + return ForegroundInfo(NOTIFICATION_ID, Notifications.makeMatoDownloadNotification(context)) } companion object{ const val TRIPS_KEYS = "tripsToDownload" - const val WORK_TAG = "tripsDownloaderAndInserter" const val DEBUG_TAG="BusTO:MatoTripDownWRK" - const val NOTIFICATION_ID=424242 + const val NOTIFICATION_ID=42424221 + + const val TAG_TRIPS ="gtfsTripsDownload" + + fun downloadTripsFromMato(trips: List, context: Context, debugTag: String): Boolean{ + if (trips.isEmpty()) return false + val workManager = WorkManager.getInstance(context) + val info = workManager.getWorkInfosForUniqueWork(TAG_TRIPS).get() + + val runNewWork = if(info.isEmpty()) true + else info[0].state!= WorkInfo.State.RUNNING && info[0].state!= WorkInfo.State.ENQUEUED + val addDat = if(info.isEmpty()) + null else info[0].state + Log.d(debugTag, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat") + if(runNewWork) { + val tripsArr = trips.toTypedArray() + val dataBuilder = Data.Builder().putStringArray(TRIPS_KEYS, tripsArr) + //build() + val requ = OneTimeWorkRequest.Builder(MatoTripsDownloadWorker::class.java) + .setInputData(dataBuilder.build()).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(TAG_TRIPS) + .build() + workManager.enqueueUniqueWork(TAG_TRIPS, ExistingWorkPolicy.KEEP, requ) + } + return true + } } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt index 81d991d..b1c02c6 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -1,144 +1,156 @@ /* 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.gtfs import androidx.lifecycle.LiveData import androidx.room.* @Dao interface GtfsDBDao { // get queries @Query("SELECT * FROM "+GtfsRoute.DB_TABLE) fun getAllRoutes() : LiveData> @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} IN (:routeGtfsIds)") fun getRoutesByIDs(routeGtfsIds: List): LiveData> @Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE) fun getAllTripsIDs() : List @Query("SELECT "+GtfsStop.COL_STOP_ID+" FROM "+GtfsStop.DB_TABLE) fun getAllStopsIDs() : List @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID") fun getStopByStopID(queryID: String): LiveData> @Query("SELECT * FROM "+GtfsShape.DB_TABLE+ " WHERE "+GtfsShape.COL_SHAPE_ID+" LIKE :shapeID"+ " ORDER BY "+GtfsShape.COL_POINT_SEQ+ " ASC" ) fun getShapeByID(shapeID: String) : LiveData> @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_AGENCY_ID} LIKE :agencyID") fun getRoutesByAgency(agencyID:String) : LiveData> + @Query("SELECT ${MatoPattern.COL_CODE} FROM ${MatoPattern.TABLE_NAME} ") + fun getPatternsCodes(): List @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID") fun getPatternsLiveDataByRouteID(routeID: String): LiveData> @Query("SELECT * FROM "+MatoPattern.TABLE_NAME+" WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeGtfsId") fun getPatternsForRouteID(routeGtfsId: String) : List @Query("SELECT * FROM ${PatternStop.TABLE_NAME} WHERE ${PatternStop.COL_PATTERN_ID} LIKE :patternGtfsID") fun getStopsByPatternID(patternGtfsID: String): LiveData> @Transaction @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID") fun getPatternsWithStopsByRouteID(routeID: String): LiveData> @Transaction @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:patternGtfsIDs)") fun getPatternsWithStopsFromIDs(patternGtfsIDs: List) : LiveData> + @Query("SELECT ${MatoPattern.COL_CODE} FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:codes)") + fun getPatternsCodesInTheDB(codes: List): List + @Transaction @Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:tripsIds)") fun getTripsFromIDs(tripsIds: List) : List @Transaction @Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:trips)") fun getTripPatternStops(trips: List): LiveData> + + fun getRoutesForFeed(feed:String): LiveData>{ val agencyID = "${feed}:%" return getRoutesByAgency(agencyID) } + + @Transaction fun clearAndInsertRoutes(routes: List){ deleteAllRoutes() insertRoutes(routes) } @Transaction @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertRoutes(routes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStops(stops: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertCalendarServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertShapes(shapes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertDates(dates: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertTrips(trips: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStopTimes(stopTimes: List) @Query("DELETE FROM "+GtfsRoute.DB_TABLE) fun deleteAllRoutes() @Query("DELETE FROM "+GtfsStop.DB_TABLE) fun deleteAllStops() @Query("DELETE FROM "+GtfsTrip.DB_TABLE) fun deleteAllTrips() + + @Query("DELETE FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:codes)") + fun deletePatternsWithCodes(codes: List): Int @Update(onConflict = OnConflictStrategy.REPLACE) fun updateShapes(shapes: List) : Int @Transaction fun updateAllStops(stops: List){ deleteAllStops() insertStops(stops) } @Query("DELETE FROM "+GtfsStopTime.DB_TABLE) fun deleteAllStopTimes() @Query("DELETE FROM "+GtfsService.DB_TABLE) fun deleteAllServices() @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertFeeds(feeds: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAgencies(agencies: List) @Transaction fun insertAgenciesWithFeeds(feeds: List, agencies: List){ insertFeeds(feeds) insertAgencies(agencies) } //patterns @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertPatterns(patterns: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertPatternStops(patternStops: List) } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt index 4c2b51c..4bb78ae 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt @@ -1,136 +1,136 @@ /* 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.gtfs import androidx.room.* @Entity(tableName = GtfsTrip.DB_TABLE, foreignKeys=[ ForeignKey(entity = GtfsRoute::class, parentColumns = [GtfsRoute.COL_ROUTE_ID], childColumns = [GtfsTrip.COL_ROUTE_ID], onDelete = ForeignKey.CASCADE), // The service_id: ID referencing calendar.service_id or calendar_dates.service_id /* ForeignKey(entity = GtfsService::class, parentColumns = [GtfsService.COL_SERVICE_ID], childColumns = [GtfsTrips.COL_SERVICE_ID], onDelete = GtfsDatabase.FOREIGNKEY_ONDELETE), */ ], indices = [Index(GtfsTrip.COL_ROUTE_ID), Index(GtfsTrip.COL_TRIP_ID)] ) data class GtfsTrip( @ColumnInfo(name = COL_ROUTE_ID ) val routeID: String, @ColumnInfo(name = COL_SERVICE_ID) val serviceID: String, @PrimaryKey @ColumnInfo(name = COL_TRIP_ID) val tripID: String, @ColumnInfo(name = COL_HEADSIGN) val tripHeadsign: String, @ColumnInfo(name = COL_DIRECTION_ID) val directionID: Int, @ColumnInfo(name = COL_BLOCK_ID) val blockID: String, @ColumnInfo(name = COL_SHAPE_ID) val shapeID: String, @ColumnInfo(name = COL_WHEELCHAIR) val isWheelchairAccess: WheelchairAccess, @ColumnInfo(name = COL_LIMITED_R) val isLimitedRoute: Boolean, @ColumnInfo(name= COL_PATTERN_ID, defaultValue = "") val patternId: String, @ColumnInfo(name = COL_SEM_HASH) val semanticHash: String?, ): GtfsTable { constructor(valuesByColumn: Map) : this( valuesByColumn[COL_ROUTE_ID]!!, valuesByColumn[COL_SERVICE_ID]!!, valuesByColumn[COL_TRIP_ID]!!, valuesByColumn[COL_HEADSIGN]!!, valuesByColumn[COL_DIRECTION_ID]?.toIntOrNull()?: 0, valuesByColumn[COL_BLOCK_ID]!!, valuesByColumn[COL_SHAPE_ID]!!, Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR]), Converters.fromStringNum(valuesByColumn[COL_LIMITED_R], false), valuesByColumn[COL_PATTERN_ID]?:"", valuesByColumn[COL_SEM_HASH], ) companion object{ const val DB_TABLE="gtfs_trips" const val COL_ROUTE_ID="route_id" const val COL_SERVICE_ID="service_id" const val COL_TRIP_ID = "trip_id" const val COL_HEADSIGN="trip_headsign" //const val COL_SHORT_NAME="trip_short_name", const val COL_DIRECTION_ID="direction_id" const val COL_BLOCK_ID="block_id" const val COL_SHAPE_ID = "shape_id" const val COL_WHEELCHAIR="wheelchair_accessible" const val COL_LIMITED_R="limited_route" const val COL_PATTERN_ID="pattern_code" const val COL_SEM_HASH="semantic_hash" val COLUMNS= arrayOf( COL_ROUTE_ID, COL_SERVICE_ID, COL_TRIP_ID, COL_HEADSIGN, COL_DIRECTION_ID, COL_BLOCK_ID, COL_SHAPE_ID, COL_WHEELCHAIR, COL_LIMITED_R ) /* open fun fromContentValues(values: ContentValues) { val tripItem = GtfsTrips(); } */ } override fun getColumns(): Array { return COLUMNS } } data class TripAndPatternWithStops( @Embedded val trip: GtfsTrip, @Relation( parentColumn = GtfsTrip.COL_PATTERN_ID, entityColumn = MatoPattern.COL_CODE ) - val pattern: MatoPattern, + val pattern: MatoPattern?, @Relation( parentColumn = MatoPattern.COL_CODE, entityColumn = PatternStop.COL_PATTERN_ID, ) var stopsIndices: List){ init { stopsIndices = stopsIndices.sortedBy { p-> p.order } } } \ 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 bea3d02..3d38da1 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,824 +1,825 @@ /* 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.MatoPattern; 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); + mapViewModel.downloadTripsFromMato(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){ noPatternsTrips.add(tripID); } - marker.setInfoWindow(new BusInfoWindow(map, update, tripWithPatternStops != null ? tripWithPatternStops.getPattern() : null, new BusInfoWindow.onTouchUp() { - @Override - public void onActionUp() { + MatoPattern markerPattern = null; + if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) + markerPattern = tripWithPatternStops.getPattern(); + marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , () -> { - } })); 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; } } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt index 9d0d183..bdff94f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapViewModel.kt @@ -1,237 +1,209 @@ /* BusTO - View Models 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.fragments import android.app.Application import android.util.Log import androidx.lifecycle.* -import androidx.work.* import com.android.volley.Response -import com.google.transit.realtime.GtfsRealtime.VehiclePosition import it.reyboz.bustorino.backend.NetworkVolleyManager -import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest import it.reyboz.bustorino.data.* -import it.reyboz.bustorino.data.gtfs.GtfsTrip import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.concurrent.Executors /** * View Model for the map. For containing the stops, the trips and whatever */ class MapViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) - private val executor = Executors.newFixedThreadPool(2) - private val oldRepo= OldDataRepository(executor, NextGenDB.getInstance(application)) - private val matoRepository = MatoRepository(application) private val netVolleyManager = NetworkVolleyManager.getInstance(application) val positionsLiveData = MutableLiveData>() private val positionsRequestRunning = MutableLiveData() private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList?) { Log.i(DEBUG_TI,"Got response from the GTFS RT server") response?.let {it:ArrayList -> if (it.size == 0) { Log.w(DEBUG_TI,"No position updates from the server") return } else { //Log.i(DEBUG_TI, "Posting value to positionsLiveData") viewModelScope.launch { positionsLiveData.postValue(it) } } } //whatever the result, launch again the update TODO } } private val positionRequestErrorListener = Response.ErrorListener { //error listener, it->VolleyError Log.e(DEBUG_TI, "Could not download the update, error:\n"+it.stackTrace) //TODO: launch again if needed } fun requestUpdates(){ if(positionsRequestRunning.value == null || !positionsRequestRunning.value!!) { val request = GtfsRtPositionsRequest(positionRequestErrorListener, positionRequestListener) netVolleyManager.requestQueue.add(request) Log.i(DEBUG_TI, "Requested GTFS realtime position updates") positionsRequestRunning.value = true } } /*suspend fun requestDelayedUpdates(timems: Long){ delay(timems) requestUpdates() } */ fun requestDelayedUpdates(timems: Long){ viewModelScope.launch { delay(timems) requestUpdates() } } // TRIPS IDS that have to be queried to the DB val tripsIDsInUpdates : LiveData> = positionsLiveData.map { Log.i(DEBUG_TI, "positionsLiveData changed") //allow new requests for the positions of buses positionsRequestRunning.value = false //add "gtt:" prefix because it's implicit in GTFS Realtime API return@map it.map { pos -> "gtt:"+pos.tripID } } - // trips that are in the DB - val gtfsTripsInDB = tripsIDsInUpdates.switchMap { + //this holds the trips that have been downloaded but for which we have no pattern + /*private val gtfsTripsInDBMissingPattern = tripsIDsInUpdates.map { tripsIDs -> + val tripsInDB = gtfsRepo.gtfsDao.getTripsFromIDs(tripsIDs) + val tripsPatternCodes = tripsInDB.map { tr -> tr.patternId } + val codesInDB = gtfsRepo.gtfsDao.getPatternsCodesInTheDB(tripsPatternCodes) + + tripsInDB.filter { !(codesInDB.contains(it.patternId)) } + }*/ + //private val patternsCodesInDB = tripsDBPatterns.map { gtfsRepo.gtfsDao.getPatternsCodesInTheDB(it) } + + // trips that are in the DB, together with the pattern. If the pattern is not present in the DB, it's null + val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { //Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}") gtfsRepo.gtfsDao.getTripPatternStops(it) } //trip IDs to query, which are not present in the DB - val tripsGtfsIDsToQuery: LiveData> = gtfsTripsInDB.map { tripswithPatterns -> + val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") if (tripsIDsInUpdates.value!=null) return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) } else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() } } - val updatesWithTripAndPatterns = gtfsTripsInDB.map { tripPatterns-> + val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") val mdict = HashMap>() + //missing patterns + val routesToDownload = HashSet() if(positionsLiveData.value!=null) for(update in positionsLiveData.value!!){ val trID = update.tripID var found = false for(trip in tripPatterns){ + if (trip.pattern == null){ + //pattern is null, which means we have to download + // the pattern data from MaTO + routesToDownload.add(trip.trip.routeID) + } if (trip.trip.tripID == "gtt:$trID"){ found = true //insert directly mdict[trID] = Pair(update,trip) break } } if (!found){ + //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") //give the update anyway mdict[trID] = Pair(update,null) } } + //have to request download of missing Patterns + if (routesToDownload.size > 0){ + Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") + downloadMissingPatterns(ArrayList(routesToDownload)) + } + return@map mdict } /* There are two strategies for the queries, since we cannot query a bunch of tripIDs all together to Mato API -> we need to query each separately: 1 -> wait until they are all queried to insert in the DB 2 -> after each request finishes, insert it into the DB - Keep in mind that trips do not change very often, so they might only need to be inserted once every two months - TODO: find a way to avoid trips getting scrubbed (check if they are really scrubbed) + Keep in mind that trips DO CHANGE often, and so do the Patterns */ + fun downloadTripsFromMato(trips: List): Boolean{ + return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI) + } + fun downloadMissingPatterns(routeIds: List): Boolean{ + return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication()) + } init { - /* - //what happens when the trips to query with Mato are determined - tripsIDsQueried.addSource(tripsGtfsIDsToQuery) { tripList -> - // avoid infinite loop of querying to Mato, insert in DB and - // triggering another query update - - val tripsToQuery = - Log.d(DEBUG_TI, "Querying trips IDs to Mato: $tripsToQuery") - for (t in tripsToQuery){ - matoRepository.requestTripUpdate(t,matoTripReqErrorList, matoTripCallback) - } - tripsIDsQueried.value = tripsToQuery - } - tripsToInsert.addSource(tripsFromMato) { matoTrips -> - if (tripsIDsQueried.value == null) return@addSource - val tripsIdsToInsert = matoTrips.map { trip -> trip.tripID } - - //val setTripsToInsert = HashSet(tripsIdsToInsert) - val tripsRequested = HashSet(tripsIDsQueried.value!!) - val insertInDB = tripsRequested.containsAll(tripsIdsToInsert) - if(insertInDB){ - gtfsRepo.gtfsDao.insertTrips(matoTrips) - } - Log.d(DEBUG_TI, "MatoTrips: ${matoTrips.size}, total trips req: ${tripsRequested.size}, inserting: $insertInDB") - } - */ Log.d(DEBUG_TI, "MapViewModel created") Log.d(DEBUG_TI, "Observers of positionsLiveData ${positionsLiveData.hasActiveObservers()}") positionsRequestRunning.value = false; } fun testCascade(){ val n = ArrayList() n.add(GtfsPositionUpdate("22920721U","lala","lalal","lol",1000.0f,1000.0f, 9000.0f, 378192810192, GtfsPositionUpdate.VehicleInfo("aj","a"), null, null )) positionsLiveData.value = n } - private val queriedMatoTrips = HashSet() - private val downloadedMatoTrips = ArrayList() - private val failedMatoTripsDownload = HashSet() /** * Start downloading missing GtfsTrips and Insert them in the DB */ - fun downloadandInsertTripsInDB(trips: List): Boolean{ - if (trips.isEmpty()) return false - val workManager = WorkManager.getInstance(getApplication()) - val info = workManager.getWorkInfosForUniqueWork(MatoDownloadTripsWorker.WORK_TAG).get() - - val runNewWork = if(info.isEmpty()){ - true - } else info[0].state!=WorkInfo.State.RUNNING && info[0].state!=WorkInfo.State.ENQUEUED - val addDat = if(info.isEmpty()) - null else info[0].state - Log.d(DEBUG_TI, "Request to download and insert ${trips.size} trips, proceed: $runNewWork, workstate: $addDat") - if(runNewWork) { - val tripsArr = trips.toTypedArray() - val data = Data.Builder().putStringArray(MatoDownloadTripsWorker.TRIPS_KEYS, tripsArr).build() - val requ = OneTimeWorkRequest.Builder(MatoDownloadTripsWorker::class.java) - .setInputData(data).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .addTag(MatoDownloadTripsWorker.WORK_TAG) - .build() - workManager.enqueueUniqueWork(MatoDownloadTripsWorker.WORK_TAG, ExistingWorkPolicy.KEEP, requ) - } - return true - } + companion object{ const val DEBUG_TI="BusTO-MapViewModel" const val DEFAULT_DELAY_REQUESTS: Long=4000 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java index ea04c73..ac9dc4a 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,222 +1,220 @@ /* BusTO - Fragments components Copyright (C) 2020 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.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import androidx.preference.*; import androidx.work.OneTimeWorkRequest; -import androidx.work.OutOfQuotaPolicy; import androidx.work.WorkInfo; import androidx.work.WorkManager; import it.reyboz.bustorino.R; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.GtfsMaintenanceWorker; -import it.reyboz.bustorino.data.MatoDownloadTripsWorker; import org.jetbrains.annotations.NotNull; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show"; public final static String KEY_ARRIVALS_FETCHERS_USE = "arrivals_fetchers_use_setting"; private boolean setSummaryStartupPref = false; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN); if(startupScreenPref !=null){ if (startupScreenPref.getValue()==null){ startupScreenPref.setSummary(getString(R.string.nav_arrivals_text)); setSummaryStartupPref = true; } } //Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue()); Preference dbUpdateNow = findPreference("pref_db_update_now"); if (dbUpdateNow!=null) dbUpdateNow.setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull Preference preference) { //trigger update if(getContext()!=null) { DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true); Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show(); return true; } return false; } } ); else { Log.e("BusTO-Preferences", "Cannot find db update preference"); } Preference clearGtfsTrips = findPreference("pref_clear_gtfs_trips"); if (clearGtfsTrips != null) { clearGtfsTrips.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull @NotNull Preference preference) { if (getContext() != null) { OneTimeWorkRequest requ = GtfsMaintenanceWorker.Companion.makeOneTimeRequest(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS); WorkManager.getInstance(getContext()).enqueue(requ); WorkManager.getInstance(getContext()).getWorkInfosByTagLiveData(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS).observe(getViewLifecycleOwner(), (Observer>) workInfos -> { if(workInfos.isEmpty()) return; if(workInfos.get(0).getState()==(WorkInfo.State.SUCCEEDED)){ Toast.makeText( getContext(), R.string.all_trips_removed, Toast.LENGTH_SHORT ).show(); } }); return true; } return false; } }); } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); if (key.equals(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE)){ Log.d(TAG, "New value is: "+sharedPreferences.getStringSet(key, new HashSet<>())); } //sometimes this happens if(getContext()==null) return; if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){ ListPreference listPref = (ListPreference) pref; pref.setSummary(listPref.getEntry()); } /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } }