commit 956f1fc0378b11ffb8b2ac85fdc2e5b9c7bb2d15 Author: Fabio Mazza Date: Thu May 14 16:22:47 2026 +0200 Add database schema, remove old code, refactor asynctask Summary: Remove unreachable code Remove useless AsyncTask Replace asynctask with coroutine, merge inside FragmentHelper Fix T1429 Test Plan: Search for stop name (like, "marc" or "cast") in previous version and check that the results are the same in this diff Reviewers: #libre_busto_hackers, valerio.bozzolan Reviewed By: #libre_busto_hackers, valerio.bozzolan Subscribers: valerio.bozzolan Project Tags: #libre_busto Maniphest Tasks: T1429 Differential Revision: https://gitpull.it/D240 diff --git a/app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/4.json b/app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/4.json new file mode 100644 index 0000000..f4ae008 --- /dev/null +++ b/app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/4.json @@ -0,0 +1,950 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "329d27f1f2a9415ef1a8f87957b99e64", + "entities": [ + { + "tableName": "gtfs_feeds", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "feed_id" + ] + } + }, + { + "tableName": "gtfs_agencies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "ag_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "ag_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fareUrl", + "columnName": "fare_url", + "affinity": "TEXT" + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT" + }, + { + "fieldPath": "feed.gtfsId", + "columnName": "feed_id", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "gtfs_id" + ] + } + }, + { + "tableName": "gtfs_calendar_dates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `date` TEXT NOT NULL, `exception_type` INTEGER NOT NULL, PRIMARY KEY(`service_id`, `date`), FOREIGN KEY(`service_id`) REFERENCES `gtfs_calendar`(`service_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exception_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "service_id", + "date" + ] + }, + "foreignKeys": [ + { + "table": "gtfs_calendar", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "service_id" + ], + "referencedColumns": [ + "service_id" + ] + } + ] + }, + { + "tableName": "stops_gtfs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` INTEGER NOT NULL, `stop_code` TEXT NOT NULL, `stop_name` TEXT NOT NULL, `stop_desc` TEXT NOT NULL, `stop_lat` REAL NOT NULL, `stop_lon` REAL NOT NULL, `wheelchair_boarding` TEXT NOT NULL, PRIMARY KEY(`stop_id`))", + "fields": [ + { + "fieldPath": "internalID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gttStopID", + "columnName": "stop_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopName", + "columnName": "stop_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gttPlaceName", + "columnName": "stop_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "stop_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "stop_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wheelchair", + "columnName": "wheelchair_boarding", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stop_id" + ] + } + }, + { + "tableName": "gtfs_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `monday` INTEGER NOT NULL, `tuesday` INTEGER NOT NULL, `wednesday` INTEGER NOT NULL, `thursday` INTEGER NOT NULL, `friday` INTEGER NOT NULL, `saturday` INTEGER NOT NULL, `sunday` INTEGER NOT NULL, `start_date` TEXT NOT NULL, `end_date` TEXT NOT NULL, PRIMARY KEY(`service_id`))", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "onMonday", + "columnName": "monday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onTuesday", + "columnName": "tuesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onWednesday", + "columnName": "wednesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onThursday", + "columnName": "thursday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onFriday", + "columnName": "friday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSaturday", + "columnName": "saturday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSunday", + "columnName": "sunday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "service_id" + ] + } + }, + { + "tableName": "routes_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agencyID", + "columnName": "agency_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "route_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "route_long_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "route_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "route_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "route_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "route_text_color", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "route_id" + ] + } + }, + { + "tableName": "gtfs_stop_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`trip_id` TEXT NOT NULL, `arrival_time` TEXT NOT NULL, `departure_time` TEXT NOT NULL, `stop_id` INTEGER NOT NULL, `stop_sequence` INTEGER NOT NULL, PRIMARY KEY(`trip_id`, `stop_id`), FOREIGN KEY(`stop_id`) REFERENCES `stops_gtfs`(`stop_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`trip_id`) REFERENCES `gtfs_trips`(`trip_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrival_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departure_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopSequence", + "columnName": "stop_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "trip_id", + "stop_id" + ] + }, + "indices": [ + { + "name": "index_gtfs_stop_times_stop_id", + "unique": false, + "columnNames": [ + "stop_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_stop_times_stop_id` ON `${TABLE_NAME}` (`stop_id`)" + } + ], + "foreignKeys": [ + { + "table": "stops_gtfs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stop_id" + ], + "referencedColumns": [ + "stop_id" + ] + }, + { + "table": "gtfs_trips", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "trip_id" + ], + "referencedColumns": [ + "trip_id" + ] + } + ] + }, + { + "tableName": "gtfs_trips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `service_id` TEXT NOT NULL, `trip_id` TEXT NOT NULL, `trip_headsign` TEXT NOT NULL, `direction_id` INTEGER NOT NULL, `block_id` TEXT NOT NULL, `shape_id` TEXT NOT NULL, `wheelchair_accessible` TEXT NOT NULL, `limited_route` INTEGER NOT NULL, `pattern_code` TEXT NOT NULL DEFAULT '', `semantic_hash` TEXT, PRIMARY KEY(`trip_id`), FOREIGN KEY(`route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routeID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripHeadsign", + "columnName": "trip_headsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionID", + "columnName": "direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockID", + "columnName": "block_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isWheelchairAccess", + "columnName": "wheelchair_accessible", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLimitedRoute", + "columnName": "limited_route", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patternId", + "columnName": "pattern_code", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "semanticHash", + "columnName": "semantic_hash", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "trip_id" + ] + }, + "indices": [ + { + "name": "index_gtfs_trips_route_id", + "unique": false, + "columnNames": [ + "route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_route_id` ON `${TABLE_NAME}` (`route_id`)" + }, + { + "name": "index_gtfs_trips_trip_id", + "unique": false, + "columnNames": [ + "trip_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_trip_id` ON `${TABLE_NAME}` (`trip_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "gtfs_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shape_id` TEXT NOT NULL, `shape_pt_lat` REAL NOT NULL, `shape_pt_lon` REAL NOT NULL, `shape_pt_sequence` INTEGER NOT NULL, PRIMARY KEY(`shape_id`, `shape_pt_sequence`))", + "fields": [ + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointLat", + "columnName": "shape_pt_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointLon", + "columnName": "shape_pt_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointSequence", + "columnName": "shape_pt_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "shape_id", + "shape_pt_sequence" + ] + } + }, + { + "tableName": "mato_patterns", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "pattern_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "pattern_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "semanticHash", + "columnName": "pattern_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "pattern_direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "routeGtfsId", + "columnName": "pattern_route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "pattern_headsign", + "affinity": "TEXT" + }, + { + "fieldPath": "patternGeometryPoly", + "columnName": "pattern_polyline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patternGeometryLength", + "columnName": "pattern_polylength", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pattern_code" + ] + }, + "indices": [ + { + "name": "index_mato_patterns_pattern_code", + "unique": false, + "columnNames": [ + "pattern_code" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mato_patterns_pattern_code` ON `${TABLE_NAME}` (`pattern_code`)" + }, + { + "name": "index_mato_patterns_pattern_route_id", + "unique": false, + "columnNames": [ + "pattern_route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mato_patterns_pattern_route_id` ON `${TABLE_NAME}` (`pattern_route_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "patterns_stops", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "patternId", + "columnName": "pattern_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopGtfsId", + "columnName": "stop_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "stop_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pattern_gtfs_id", + "stop_gtfs_id", + "stop_order" + ] + }, + "indices": [ + { + "name": "index_patterns_stops_pattern_gtfs_id", + "unique": false, + "columnNames": [ + "pattern_gtfs_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_patterns_stops_pattern_gtfs_id` ON `${TABLE_NAME}` (`pattern_gtfs_id`)" + } + ], + "foreignKeys": [ + { + "table": "mato_patterns", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_gtfs_id" + ], + "referencedColumns": [ + "pattern_code" + ] + } + ] + }, + { + "tableName": "gtfsrt_alerts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `cause` INTEGER NOT NULL, `effect` INTEGER NOT NULL, `fetchedAt` INTEGER NOT NULL, `userSeen` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cause", + "columnName": "cause", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "effect", + "columnName": "effect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fetchedAt", + "columnName": "fetchedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSeen", + "columnName": "userSeen", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "gtfsrt_alert_translations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` TEXT NOT NULL, `alertId` TEXT NOT NULL, `field` TEXT NOT NULL, `language` TEXT, `text` TEXT NOT NULL, PRIMARY KEY(`hash`), FOREIGN KEY(`alertId`) REFERENCES `gtfsrt_alerts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alertId", + "columnName": "alertId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "field", + "columnName": "field", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hash" + ] + }, + "indices": [ + { + "name": "index_gtfsrt_alert_translations_alertId", + "unique": false, + "columnNames": [ + "alertId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfsrt_alert_translations_alertId` ON `${TABLE_NAME}` (`alertId`)" + } + ], + "foreignKeys": [ + { + "table": "gtfsrt_alerts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "alertId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "alerts_active_periods", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` TEXT NOT NULL, `alertId` TEXT NOT NULL, `start` INTEGER, `end` INTEGER, PRIMARY KEY(`hash`), FOREIGN KEY(`alertId`) REFERENCES `gtfsrt_alerts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alertId", + "columnName": "alertId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER" + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hash" + ] + }, + "indices": [ + { + "name": "index_alerts_active_periods_alertId", + "unique": false, + "columnNames": [ + "alertId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_active_periods_alertId` ON `${TABLE_NAME}` (`alertId`)" + } + ], + "foreignKeys": [ + { + "table": "gtfsrt_alerts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "alertId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "alerts_informed_entities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `alertId` TEXT NOT NULL, `routeId` TEXT, `routeType` INTEGER, `stopId` TEXT, `tripId` TEXT, `tripRouteId` TEXT, `directionId` INTEGER, PRIMARY KEY(`internalId`), FOREIGN KEY(`alertId`) REFERENCES `gtfsrt_alerts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alertId", + "columnName": "alertId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "routeId", + "columnName": "routeId", + "affinity": "TEXT" + }, + { + "fieldPath": "routeType", + "columnName": "routeType", + "affinity": "INTEGER" + }, + { + "fieldPath": "stopId", + "columnName": "stopId", + "affinity": "TEXT" + }, + { + "fieldPath": "tripId", + "columnName": "tripId", + "affinity": "TEXT" + }, + { + "fieldPath": "tripRouteId", + "columnName": "tripRouteId", + "affinity": "TEXT" + }, + { + "fieldPath": "directionId", + "columnName": "directionId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_alerts_informed_entities_alertId", + "unique": false, + "columnNames": [ + "alertId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_informed_entities_alertId` ON `${TABLE_NAME}` (`alertId`)" + }, + { + "name": "index_alerts_informed_entities_routeId", + "unique": false, + "columnNames": [ + "routeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_informed_entities_routeId` ON `${TABLE_NAME}` (`routeId`)" + }, + { + "name": "index_alerts_informed_entities_stopId", + "unique": false, + "columnNames": [ + "stopId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_informed_entities_stopId` ON `${TABLE_NAME}` (`stopId`)" + }, + { + "name": "index_alerts_informed_entities_tripId", + "unique": false, + "columnNames": [ + "tripId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_alerts_informed_entities_tripId` ON `${TABLE_NAME}` (`tripId`)" + } + ], + "foreignKeys": [ + { + "table": "gtfsrt_alerts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "alertId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '329d27f1f2a9415ef1a8f87957b99e64')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java index 5c3b498..5d412b7 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -29,10 +29,7 @@ import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; -import it.reyboz.bustorino.backend.Fetcher; -import it.reyboz.bustorino.backend.Palina; -import it.reyboz.bustorino.backend.Stop; -import it.reyboz.bustorino.backend.utils; +import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; @@ -52,7 +49,7 @@ public class FragmentHelper { private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; - private WeakReference lastTaskRef; + private final StopSearcher stopSearcher; private boolean shouldHaltAllActivities=false; @@ -66,6 +63,7 @@ public class FragmentHelper { this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); + stopSearcher = new StopSearcher(this); } /** @@ -81,9 +79,6 @@ public class FragmentHelper { this.lastSuccessfullySearchedBusStop = stop; } - public void setLastTaskRef(AsyncTask task) { - this.lastTaskRef = new WeakReference<>(task); - } /** * Called when you need to create a fragment for a specified Palina @@ -199,12 +194,19 @@ public class FragmentHelper { this.shouldHaltAllActivities = shouldI; } - public void stopLastRequestIfNeeded(boolean interruptIfRunning){ - if(lastTaskRef == null) return; + public void stopLastRequestIfNeeded(){ + /*if(lastTaskRef == null) return; AsyncTask task = lastTaskRef.get(); if(task!=null){ task.cancel(interruptIfRunning); } + + */ + stopSearcher.cancelLastRequest(); + } + public void requestStopSearch(String query){ + stopSearcher.cancelLastRequest(); + stopSearcher.runRequest(query, new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); // run with the default fetchers } /** diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index b0e5c7d..429830f 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -59,8 +59,6 @@ import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; -import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; -import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; @@ -109,31 +107,25 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// private FragmentManager childFragMan; - Handler mainHandler; - private final Runnable refreshStop = new Runnable() { - public void run() { - if(getContext() == null) return; - - if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { - ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); - if (fragment == null){ - //we create a new fragment, which is WRONG - Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); - // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); - } else{ - //String stopName = fragment.getStopID(); - - //new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); - fragment.requestArrivalsForTheFragment(); - } - } else { //we create a new fragment, which is WRONG - List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); - ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; - arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); - new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); + + private void refreshStop() { + if(getContext() == null){ + Log.w(DEBUG_TAG,"Asked to refresh stop but context is null"); + return; + } + if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { + ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); + if (fragment == null){ + //we create a new fragment, which is WRONG + Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); + } else{ + //String stopName = fragment.getStopID(); + fragment.requestArrivalsForTheFragment(); } + } else { //we create a new fragment, which is WRONG + Log.w(DEBUG_TAG, "Asked to refresh stop when there is no fragment"); } - }; + } // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { @@ -203,58 +195,6 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL }); - //private final LocationCriteria cr = new LocationCriteria(2000, 10000); - //Location - /*private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { - @Override - public void onLocationChanged(Location loc) { - - } - - @Override - public void onLocationStatusChanged(int status) { - - if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ - //request Stops - //pendingNearbyStopsRequest = false; - if (getContext()!= null && !isNearbyFragmentShown()) - //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfPossible(); - } - } - - @Override - public long getLastUpdateTimeMillis() { - return 50; - } - - @Override - public LocationCriteria getLocationCriteria() { - return cr; - } - - @Override - public void onLocationProviderAvailable() { - //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); - if(!isNearbyFragmentShown() && getContext()!=null){ - // we should have the location permission - if(!checkLocationPermission()) - Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + - "we have no location permission"); - pendingNearbyStopsFragmentRequest = true; - //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfPossible(); - } - } - - @Override - public void onLocationDisabled() { - - } - }; - - */ - //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; @@ -315,7 +255,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL }); swipeRefreshLayout - .setOnRefreshListener(() -> mainHandler.post(refreshStop)); + .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); @@ -405,7 +345,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); - fragmentHelper.stopLastRequestIfNeeded(true); + fragmentHelper.stopLastRequestIfNeeded(); toggleSpinner(false); } @@ -415,7 +355,6 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); - mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { @@ -531,9 +470,9 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL public void onPause() { //mainHandler = null; //locationManager.removeLocationRequestFor(requester); - super.onPause(); fragmentHelper.setBlockAllActivities(true); - fragmentHelper.stopLastRequestIfNeeded(true); + fragmentHelper.stopLastRequestIfNeeded(); + super.onPause(); } @@ -564,10 +503,10 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL * @param v View clicked */ public void onSearchClick(View v) { - final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; + //final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); - fragmentHelper.stopLastRequestIfNeeded(true); + fragmentHelper.stopLastRequestIfNeeded(); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); @@ -579,8 +518,7 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { - fragmentHelper.stopLastRequestIfNeeded(true); - new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); + fragmentHelper.requestStopSearch(query); } } } @@ -831,72 +769,5 @@ public class MainScreenFragment extends ScreenBaseFragment implements FragmentL pendingNearbyStopsFragmentRequest = false; } } - /////////// LOCATION METHODS ////////// - - /* - private void startStopRequest(String provider) { - Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); - if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { - - } - } - - */ - /* - - * Run location requests separately and asynchronously - - class NearbyStopsRequester implements Runnable { - Context appContext; - Criteria cr; - - public NearbyStopsRequester(Context appContext, Criteria criteria) { - this.appContext = appContext.getApplicationContext(); - this.cr = criteria; - } - - @Override - public void run() { - if(isNearbyFragmentShown()) { - //nothing to do - Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); - return; - } - - final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; - final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; - - //if we don't have the permission, we have to ask for it, if we haven't - // asked too many times before - if (noPermission) { - if (!isOldVersion) { - pendingNearbyStopsRequest = true; - //Permissions.assertLocationPermissions(appContext,getActivity()); - requestPermissionLauncher.launch(LOCATION_PERMISSIONS); - Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); - return; - } else { - Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); - } - } else setOption(LOCATION_PERMISSION_GIVEN, true); - - AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); - final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); - if (haveProviders - && fragmentHelper.getLastSuccessfullySearchedBusStop() == null - && !fragMan.isDestroyed()) { - //Go ahead with the request - Log.d("mainActivity", "Recreating stop fragment"); - showNearbyStopsFragment(); - pendingNearbyStopsRequest = false; - } else if(!haveProviders){ - Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); - } - - } - } - - */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java deleted file mode 100644 index fd47fe4..0000000 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - BusTO (middleware) - Copyright (C) 2018 Fabio Mazza - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -package it.reyboz.bustorino.middleware; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.AsyncTask; - -import androidx.annotation.NonNull; - -import android.util.Log; - -import it.reyboz.bustorino.backend.*; -import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; -import it.reyboz.bustorino.data.AppDataProvider; -import it.reyboz.bustorino.data.NextGenDB; -import it.reyboz.bustorino.fragments.FragmentHelper; -import it.reyboz.bustorino.data.NextGenDB.Contract.*; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.Calendar; - -/** - * This should be used to download data, but not to display it - */ -public class AsyncArrivalsSearcher extends AsyncTask{ - - private static final String TAG = "BusTO-DataDownload"; - private static final String DEBUG_TAG = TAG; - private boolean failedAll = false; - - private final AtomicReference finalResultRef; - private String query; - WeakReference helperRef; - private final ArrayList otherActivities = new ArrayList<>(); - private final ArrivalsFetcher[] theFetchers; - @SuppressLint("StaticFieldLeak") - private final Context context; - private final boolean replaceFragment; - - - public AsyncArrivalsSearcher(FragmentHelper fh, @NonNull ArrivalsFetcher[] fetchers, Context context) { - helperRef = new WeakReference<>(fh); - fh.setLastTaskRef(this); - finalResultRef = new AtomicReference<>(); - this.context = context.getApplicationContext(); - this.replaceFragment = true; - - theFetchers = fetchers; - if (theFetchers.length < 1){ - throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); - } - - } - - @Override - protected Palina doInBackground(String... params) { - RecursionHelper r = new RecursionHelper<>(theFetchers); - Palina resultPalina = null; - FragmentHelper fh = helperRef.get(); - ArrayList results = new ArrayList<>(theFetchers.length); - //If the FragmentHelper is null, that means the activity doesn't exist anymore - StringBuilder sb = new StringBuilder(); - for (ArrivalsFetcher f: theFetchers){ - sb.append(""); - sb.append(f.getClass().getSimpleName()); - sb.append("; "); - } - Log.d(DEBUG_TAG, "Using fetchers: "+sb.toString()); - if (fh == null){ - return null; - } - - //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); - while(r.valid()) { - if(this.isCancelled()) { - return null; - } - //get the data from the fetcher - ArrivalsFetcher f = r.getAndMoveForward(); - AtomicReference resRef = new AtomicReference<>(); - if (f instanceof ArrivalsFetcherContext){ - ((ArrivalsFetcherContext)f).setContext(context); - } - Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); - - Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); - Palina p; - String stopID; - if(params.length>0) - stopID=params[0]; //(it's a Palina) - else if(lastSearchedBusStop!=null) - stopID = lastSearchedBusStop.ID; //(it's a Palina) - else { - publishProgress(Fetcher.Result.QUERY_TOO_SHORT); - return null; - } - //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times - try { - if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) - continue; - } catch (NumberFormatException ex){ - Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); - } - p= f.ReadArrivalTimesAll(stopID,resRef); - - - //if (res.get()!= Fetcher.Result.OK) - Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+resRef.get()); - - if(f instanceof FiveTAPIFetcher){ - AtomicReference gres = new AtomicReference<>(); - List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); - Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); - if(gres.get() == Fetcher.Result.OK){ - p.addInfoFromRoutes(branches); - Thread t = new Thread(new BranchInserter(branches, context)); - t.start(); - otherActivities.add(t); - } else{ - resRef.set(Fetcher.Result.NOT_FOUND); - } - //put updated values into Database - } - - if(lastSearchedBusStop != null && resRef.get()== Fetcher.Result.OK) { - // check that we don't have the same stop - if(lastSearchedBusStop.ID.equals(p.ID)) { - // searched and it's the same - String sn = lastSearchedBusStop.getStopDisplayName(); - if(sn != null) { - // "merge" Stop over Palina and we're good to go - p.mergeNameFrom(lastSearchedBusStop); - } - } - } - p.mergeDuplicateRoutes(0); - if (resRef.get() == Fetcher.Result.OK && p.getTotalNumberOfPassages() == 0 ) { - resRef.set(Fetcher.Result.EMPTY_RESULT_SET); - Log.d(DEBUG_TAG, "Setting empty results"); - } - publishProgress(resRef.get()); - //TODO: find a way to avoid overloading the user with toasts - if (resultPalina == null && f instanceof MatoAPIFetcher && p.queryAllRoutes().size() > 0){ - resultPalina = p; - } - //find if it went well - results.add(resRef.get()); - if(resRef.get()== Fetcher.Result.OK) { - //wait for other threads to finish - for(Thread t: otherActivities){ - try { - t.join(); - } catch (InterruptedException e) { - //do nothing - } - } - return p; - } - - finalResultRef.set(resRef.get()); - } - /* - boolean emptyResults = true; - for (Fetcher.Result re: results){ - if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { - emptyResults = false; - break; - } - } - - */ - //at this point, we are sure that the result has been negative - failedAll=true; - - - return resultPalina; - } - - @Override - protected void onProgressUpdate(Fetcher.Result... values) { - FragmentHelper fh = helperRef.get(); - if (fh!=null) - for (Fetcher.Result r : values){ - //TODO: make Toast - fh.showErrorMessage(r, SearchRequestType.ARRIVALS); - } - else { - Log.w(TAG,"We had to show some progress but activity was destroyed"); - } - } - - @Override - protected void onPostExecute(Palina p) { - FragmentHelper fh = helperRef.get(); - - if(p == null || fh == null){ - //everything went bad - if(fh!=null) fh.toggleSpinner(false); - cancel(true); - //TODO: send message here - return; - } - - if(isCancelled()) return; - - - fh.createOrUpdateStopFragment( p, replaceFragment); - } - - @Override - protected void onCancelled() { - FragmentHelper fh = helperRef.get(); - if (fh!=null) fh.toggleSpinner(false); - } - - @Override - protected void onPreExecute() { - FragmentHelper fh = helperRef.get(); - if (fh!=null) fh.toggleSpinner(true); - } - - - public static class BranchInserter implements Runnable{ - private final List routesToInsert; - - private final Context context; - //private final NextGenDB nextGenDB; - - public BranchInserter(List routesToInsert,@NonNull Context con) { - this.routesToInsert = routesToInsert; - this.context = con.getApplicationContext(); - //nextGenDB = new NextGenDB(context); - } - - @Override - public void run() { - NextGenDB.insertBranchesIntoDB(context, routesToInsert); - } - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java b/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java deleted file mode 100644 index ed60ed8..0000000 --- a/app/src/main/java/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - BusTO (middleware) - 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.middleware; - -import android.os.AsyncTask; -import android.util.Log; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import it.reyboz.bustorino.backend.Fetcher; -import it.reyboz.bustorino.backend.Stop; -import it.reyboz.bustorino.backend.StopsFinderByName; -import it.reyboz.bustorino.fragments.FragmentHelper; - -public class AsyncStopsSearcher extends AsyncTask> { - - private static final String TAG = "BusTO-StopsSearcher"; - private static final String DEBUG_TAG = TAG; - private final StopsFinderByName[] fetchers; - private final AtomicReference res; - - private WeakReference helperWR; - - private String theQuery; - - public AsyncStopsSearcher(FragmentHelper fh, StopsFinderByName[] fetchers) { - this.fetchers = fetchers; - if (fetchers.length < 1){ - throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); - } - - this.res = new AtomicReference<>(); - this.helperWR = new WeakReference<>(fh); - fh.setLastTaskRef(this); - - } - - @Override - protected List doInBackground(String... strings) { - RecursionHelper r = new RecursionHelper<>(fetchers); - if (helperWR.get()==null || strings.length == 0) - return null; - Log.d(DEBUG_TAG,"Running with query "+strings[0]); - - ArrayList results = new ArrayList<>(); - List resultsList; - while (r.valid()){ - if (this.isCancelled()) return null; - - final StopsFinderByName finder = r.getAndMoveForward(); - theQuery = strings[0].trim(); - resultsList = finder.FindByName(theQuery, res); - Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultsList.size()+" stops"); - - if (res.get()== Fetcher.Result.OK){ - return resultsList; - } - results.add(res.get()); - } - boolean emptyResults = true; - for (Fetcher.Result re: results){ - if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { - emptyResults = false; - break; - } - } - if(emptyResults){ - publishProgress(Fetcher.Result.EMPTY_RESULT_SET); - } - return new ArrayList<>(); - } - - @Override - protected void onProgressUpdate(Fetcher.Result... values) { - FragmentHelper fh = helperWR.get(); - if (fh!=null) - for (Fetcher.Result r : values){ - fh.showErrorMessage(r, SearchRequestType.STOPS); - } - else { - Log.w(TAG,"We had to show some progress but activity was destroyed"); - } - } - @Override - protected void onCancelled() { - FragmentHelper fh = helperWR.get(); - if (fh!=null) fh.toggleSpinner(false); - } - - @Override - protected void onPreExecute() { - FragmentHelper fh = helperWR.get(); - if (fh!=null) fh.toggleSpinner(true); - } - - @Override - protected void onPostExecute(List stops) { - final FragmentHelper fh = helperWR.get(); - - if (stops==null || stops.size() == 0 || fh==null || theQuery==null) { - if (fh!=null) fh.toggleSpinner(false); - cancel(true); - return; - } - if(isCancelled()){ - fh.toggleSpinner(false); - return; - } - fh.createStopListFragment(stops, theQuery, true); - - - } -} diff --git a/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt b/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt new file mode 100644 index 0000000..05727a4 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/middleware/StopSearcher.kt @@ -0,0 +1,133 @@ +package it.reyboz.bustorino.middleware + +import android.content.Context +import android.util.Log +import it.reyboz.bustorino.backend.Fetcher +import it.reyboz.bustorino.backend.FiveTStopsFetcher +import it.reyboz.bustorino.backend.GTTStopsFetcher +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.StopsFinderByName +import it.reyboz.bustorino.fragments.FragmentHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.cancellation.CancellationException + +class StopSearcher( + fragmentHelper: FragmentHelper, +) { + + private val helperRef = WeakReference(fragmentHelper) + private val resultRef = AtomicReference(Fetcher.Result.PARSER_ERROR) + + private val finders = arrayOf(GTTStopsFetcher(), FiveTStopsFetcher()) + private var lastJob : Job? = null + + private suspend fun getData(query: String, r: RecursionHelper): List? { + if (helperRef.get() == null) return null + if(query.isEmpty()) { + resultRef.set(Fetcher.Result.QUERY_TOO_SHORT) + return null + } + resultRef.set(Fetcher.Result.OK) + Log.d(DEBUG_TAG, "Running with query " + query) + + //val results = ArrayList() + //var resultsList: List + val queryOk = query.trim { it <= ' ' } + while (r.valid()) { + + val finder = r.getAndMoveForward() + val resultsList = finder.FindByName(queryOk, resultRef) + Log.d(DEBUG_TAG, "Result: " + resultRef.get() + ", " + resultsList.size + " stops") + + if (resultRef.get() == Fetcher.Result.OK) { + return resultsList + } + //results.add(resultRef.get()) + } + /*var emptyResults = true + for (re in results) { + if (re != Fetcher.Result.EMPTY_RESULT_SET) { + emptyResults = false + break + } + } + if (emptyResults) { + showResultAsync(Fetcher.Result.EMPTY_RESULT_SET) + } + + */ + return listOf() + } + + + private fun showError(result: Fetcher.Result) { + val helper = helperRef.get() ?: return + helper.showErrorMessage(result, SearchRequestType.STOPS) + } + + fun runRequest(query: String, fetchers: Array?) { + //start spinner + + helperRef.get()?.toggleSpinner(true) + lastJob = CoroutineScope(Dispatchers.IO).launch{ + try { + val r = RecursionHelper(fetchers ?: finders) + val stopList = getData(query, r) + if(stopList == null) { + withContext(Dispatchers.Main) { + showError(resultRef.get()) + } + } + else if(stopList.isEmpty()) { + withContext(Dispatchers.Main) { + showError(Fetcher.Result.EMPTY_RESULT_SET) + } + } else{ + //list of stops, non-null and not empty + withContext(Dispatchers.Main) { + showResult(stopList, query) + } + } + + }catch (e: CancellationException) { + Log.d(DEBUG_TAG, "Request cancelled") + /*withContext(Dispatchers.Main) { + helperRef.get()?.toggleSpinner(false) + } + + */ + } + withContext(Dispatchers.Main) { + helperRef.get()?.toggleSpinner(false) + } + + } + } + fun runRequest(query: String) { + runRequest(query, null) + } + + fun cancelLastRequest() { + lastJob?.let{ + if(!it.isCompleted) + it.cancel() + } + } + fun showResult(stops:List, query: String) { + val helper = helperRef.get() ?: return + helper.createStopListFragment(stops,query, true) + } + + + companion object { + const val DEBUG_TAG = "BusTO-StopSearcher" + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26f4af0..f1e17ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,7 +346,7 @@ Look in the Settings to customize the app behaviour, and in the About Location permission has not been granted Missing permission - To use this functionality, the application needs access to the location, which can not only be granted in the system settings. + To use this functionality, the application needs access to the location, which now can only be granted in the system settings. Open settings OK, close the tutorial