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 @@ -1,285 +1,287 @@ /* BusTO (fragments) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import android.os.AsyncTask; 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; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; private final FragmentListenerMain listenerMain; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; 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; public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { this.listenerMain = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); + stopSearcher = new StopSearcher(this); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { 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 * @param p the Stop that needs to be displayed */ public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){ boolean sameFragment; ArrivalsFragment arrivalsFragment = null; if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } FragmentManager fm = managerWeakRef.get(); if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); //Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?"); if (arrivalsFragment == null) sameFragment = false; else sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else { sameFragment = false; Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); } setLastSuccessfullySearchedBusStop(p); if (sameFragment){ Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); if (arrivalsFragment == null) sameFragment = false; } if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } String probableTag = ArrivalsFragment.getFragmentTag(p); attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } // DO NOT CALL `setListAdapter` ever on arrivals fragment arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); listenerMain.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ listenerMain.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } attachFragmentToContainer(managerWeakRef.get(),listfragment, new AttachParameters("search_"+query, false,addToBackStack)); listfragment.setStopList(resultList); //listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ listenerMain.toggleSpinner(on); } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param parameters attach parameters */ protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){ if(shouldHaltAllActivities) //nothing to do return; FragmentTransaction ft = fm.beginTransaction(); int frameID; if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) // ft.replace(secondaryFrameLayout,fragment,tag); frameID = secondaryFrameLayout; else frameID = primaryFrameLayout; switch (parameters.transaction){ case REPLACE: ft.replace(frameID,fragment,parameters.tag); } if (parameters.addToBackStack) ft.addToBackStack("state_"+parameters.tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); if(!fm.isDestroyed() && !shouldHaltAllActivities) ft.commit(); //fm.executePendingTransactions(); } public synchronized void setBlockAllActivities(boolean shouldI) { 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 } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.Result res, SearchRequestType type){ //TODO: implement a common set of errors for all fragments if (res==null){ Log.e(DEBUG_TAG, "Asked to show result with null result"); return; } Log.d(DEBUG_TAG, "Showing result for "+res); switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: if (type == SearchRequestType.STOPS) showShortToast(R.string.no_bus_stop_have_this_name); else if(type == SearchRequestType.ARRIVALS){ showShortToast(R.string.no_arrivals_stop); } break; case NOT_FOUND: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } enum Transaction{ REPLACE, } static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } } 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 @@ -1,902 +1,773 @@ /* BusTO - Fragments 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.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; -import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; -import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; //private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE! private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// 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 -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; //AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() { @Override public void onActivityResult(Map result) { if (result == null) return; if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: " + result); if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) { locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); /*if (mListener != null && getContext() != null) { if (locationManager == null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } */ // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if (pendingNearbyStopsFragmentRequest) { showNearbyFragmentIfPossible(); pendingNearbyStopsFragmentRequest = false; } } if (pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest = false; } }); - //private final LocationCriteria cr = new LocationCriteria(2000, 10000); - //Location - /*private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { - @Override - public void onLocationChanged(Location loc) { - - } - - @Override - public void onLocationStatusChanged(int status) { - - if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ - //request Stops - //pendingNearbyStopsRequest = false; - if (getContext()!= null && !isNearbyFragmentShown()) - //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfPossible(); - } - } - - @Override - public long getLastUpdateTimeMillis() { - return 50; - } - - @Override - public LocationCriteria getLocationCriteria() { - return cr; - } - - @Override - public void onLocationProviderAvailable() { - //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); - if(!isNearbyFragmentShown() && getContext()!=null){ - // we should have the location permission - if(!checkLocationPermission()) - Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + - "we have no location permission"); - pendingNearbyStopsFragmentRequest = true; - //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); - showNearbyFragmentIfPossible(); - } - } - - @Override - public void onLocationDisabled() { - - } - }; - - */ - //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout - .setOnRefreshListener(() -> mainHandler.post(refreshStop)); + .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff childFragMan = getChildFragmentManager(); childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); /* cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); */ //locationManager = AppLocationManager.getInstance(requireContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); - fragmentHelper.stopLastRequestIfNeeded(true); + fragmentHelper.stopLastRequestIfNeeded(); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); - mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){ Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } } else { //The Introductory Activity is about to be started, hence pause the request and show later pendingIntroRun = true; } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); //if (locationManager == null) // locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ //request position permission if needed if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } //deactivate flag pendingIntroRun = false; } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); //if(!locationManager.isRequesterRegistered(requester)) // locationManager.addLocationRequestFor(requester); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); //TODO: if we come back to this from another fragment, and the user has given again the permission // for the Location, we should show the Nearby Stops if(!suppressArrivalsReload && pendingStopID==null){ //none of the following cases are true // check if we are showing any fragment final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ //we are not showing anything if(Permissions.anyLocationPermissionsGranted(getContext())){ showNearbyFragmentIfPossible(); } } } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; //locationManager.removeLocationRequestFor(requester); - super.onPause(); fragmentHelper.setBlockAllActivities(true); - fragmentHelper.stopLastRequestIfNeeded(true); + fragmentHelper.stopLastRequestIfNeeded(); + super.onPause(); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { - final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; + //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(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { - fragmentHelper.stopLastRequestIfNeeded(true); - new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); + fragmentHelper.requestStopSearch(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForArrivals() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { //locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForArrivals(); break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity mListener.openLineFromStop(routeGtfsId, stopIDFrom); } @Override public void openLineFromVehicle(String routeGtfsId, @Nullable String optionalPatternId, @Nullable Bundle args) { mListener.openLineFromVehicle(routeGtfsId, optionalPatternId, args); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() fragment.requestArrivalsForTheFragment(); } else{ //SHOW NEW ARRIVALS FRAGMENT //new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); //prepareGUIForArrivals(); //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfPossible() { if (isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } if (fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !childFragMan.isDestroyed()) { //Go ahead with the request actuallyShowNearbyStopsFragment(); pendingNearbyStopsFragmentRequest = false; } } - /////////// LOCATION METHODS ////////// - - /* - private void startStopRequest(String provider) { - Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); - if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { - - } - } - - */ - /* - - * Run location requests separately and asynchronously - - class NearbyStopsRequester implements Runnable { - Context appContext; - Criteria cr; - - public NearbyStopsRequester(Context appContext, Criteria criteria) { - this.appContext = appContext.getApplicationContext(); - this.cr = criteria; - } - - @Override - public void run() { - if(isNearbyFragmentShown()) { - //nothing to do - Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); - return; - } - - final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; - final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; - - //if we don't have the permission, we have to ask for it, if we haven't - // asked too many times before - if (noPermission) { - if (!isOldVersion) { - pendingNearbyStopsRequest = true; - //Permissions.assertLocationPermissions(appContext,getActivity()); - requestPermissionLauncher.launch(LOCATION_PERMISSIONS); - Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); - return; - } else { - Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); - } - } else setOption(LOCATION_PERMISSION_GIVEN, true); - - AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); - final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); - if (haveProviders - && fragmentHelper.getLastSuccessfullySearchedBusStop() == null - && !fragMan.isDestroyed()) { - //Go ahead with the request - Log.d("mainActivity", "Recreating stop fragment"); - showNearbyStopsFragment(); - pendingNearbyStopsRequest = false; - } else if(!haveProviders){ - Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); - } - - } - } - - */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/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 @@ -1,396 +1,396 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop has this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Arrivals at: Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines No lines found in this category No lines match the searched name Destination: Lines: %1$s Line %1$s Line %1$s towards: Stop %1$s Vehicle %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Open the wiki https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, an open source and independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


Introductory tutorial

If you want to see the introduction again, use the button below:

]]>
News and Updates

On the Telegram channel, you can find information about the latest app updates

]]>
How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google for icons and support and design libraries.
- Other icons from Bootstrap, Feather, and Hero Icons.
- All the contributors, and the beta testers, too!


If you want more information or want to contribute to development, use the buttons below! ]]>
Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Show line details Show full direction Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device No GPS receiver found on the device! Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Show introduction Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s Loading arrivals from %1$s GTT App GTT Website 5T Torino website Muoversi a Torino Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database BusTO - live position service Live positions Showing activity related to the live positions service MaTO live bus positions service is running Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Filter by name Launching database update Downloading data from MaTO server Downloading realtime alerts data Capitalize directions @string/directions_capitalize_no_change @string/directions_capitalize_everything @string/directions_capitalize_first_letter Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might have errors) GTFS RT (less frequently updated, more accurate) @string/positions_source_mato_descr @string/positions_source_gtfsrt_descr "You are too far, not showing your position Style of the map Versatiles (vector) OSM legacy (raster, lighter) @string/map_style_versatiles @string/map_style_legacy_raster Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted 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 Close the tutorial Enable notifications Notifications enabled Backup and restore Import/export preferences Data saved Backup to file Import data from backup Backup has been imported Check at least one item to import! Import favorites from backup Import preferences from backup Hello blank fragment No map app present to show the stop! Direction is already shown Loading destination… Destination unknown Service for positions working normally No positions received Error connecting to the server Error parsing the server response Error: network response is not as expected Connecting... MaTO GTFS RT Live positions source: Switch source Clear bus positions when switching live positions source Updated: %1$s Alerts for line %1$s: No alerts in your language, showing in %1$s Italian English Checking new alerts now Press back again to close the app