diff --git a/build.gradle b/build.gradle index 41968bd..2e94aa4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,144 +1,144 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } ext { //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.3.6" activity_version = "1.2.4" appcompat_version = "1.3.1" preference_version = "1.1.1" work_version = "2.5.0" acra_version = "5.7.0" lifecycle_version = "2.3.1" arch_version = "2.1.0" room_version = "2.3.0" //kotlin kotlin_version = '1.5.0' coroutines_version = "1.5.0" } dependencies { classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() mavenCentral() } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 buildToolsVersion '29.0.3' defaultConfig { applicationId "it.reyboz.bustorino" - minSdkVersion 15 + minSdkVersion 16 targetSdkVersion 29 versionCode 35 versionName "1.15.4" vectorDrawables.useSupportLibrary = true multiDexEnabled true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { jcenter() mavenLocal() } dependencies { //new libraries implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "com.google.android.material:material:1.4.0" - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' - implementation 'com.android.volley:volley:1.2.0' + implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.14.0' // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" // Room components implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" androidTestImplementation "androidx.room:room-testing:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" } \ No newline at end of file diff --git a/res/layout/entry_bus_line_passage.xml b/res/layout/entry_bus_line_passage.xml index 00a13ef..5431b72 100644 --- a/res/layout/entry_bus_line_passage.xml +++ b/res/layout/entry_bus_line_passage.xml @@ -1,51 +1,53 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingTop="8dip" android:paddingBottom="8dip" android:paddingLeft="16dip" android:paddingRight="16dip"> <TextView android:id="@+id/routeID" android:layout_width="54dip" android:layout_height="54dip" android:background="@drawable/route_background_bus" android:gravity="center" android:textColor="@color/grey_100" - android:textSize="21sp"> + android:textSize="21sp" + android:layout_marginEnd="4dp" + android:layout_marginRight="4dp"> </TextView> <!--the icon comes from setCompoundDrawables in PalinaAdapter --> <!--android:drawableLeft="@drawable/bus" android:drawableStart="@drawable/bus" --> <TextView android:id="@+id/routeDestination" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="@color/grey_600" android:layout_height="30dp" android:layout_width="match_parent" android:layout_toEndOf="@id/routeID" android:layout_toRightOf="@id/routeID" android:layout_alignTop="@+id/routeID" android:maxLines="1" android:drawablePadding="0dp" android:singleLine="true"> </TextView> <!-- this can hold 3-4 timetable entries before overflowing into a second line. It's ugly but at least doesn't lose any information. --> <TextView android:id="@+id/routesThatStopHere" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="@color/blue_500" android:layout_height="wrap_content" android:layout_width="match_parent" android:layout_marginLeft="5dip" android:layout_marginStart="5dip" android:layout_toEndOf="@id/routeID" android:layout_toRightOf="@id/routeID" android:layout_below="@id/routeDestination"> </TextView> </RelativeLayout> \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 3eedbb2..ab2955f 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,158 +1,168 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_description">Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy.</string> <string name="search">Cerca</string> <string name="qrcode">QR Code</string> <string name="insert_bus_stop_number">Numero fermata</string> <string name="insert_bus_stop_name">Nome fermata</string> <string name="insert_bus_stop_number_error">Inserisci il numero della fermata</string> <string name="insert_bus_stop_name_error">Inserisci il nome della fermata</string> <string name="network_error">Verifica l\'accesso ad Internet!</string> <string name="no_bus_stop_have_this_name">Sembra che nessuna fermata abbia questo nome</string> <string name="parsing_error">Errore di lettura del sito 5T/GTT (dannato sito!)</string> <string name="passages">Fermata: %1$s</string> <string name="lines">Linee: %1$s</string> <string name="results">Scegli la fermata…</string> <string name="no_passages">Nessun passaggio</string> <string name="no_qrcode">Nessun QR code</string> <string name="action_favorites">Preferiti</string> <string name="action_help">Aiuto</string> <string name="action_about">Informazioni</string> <string name="action_about_more">Più informazioni</string> <string name="action_wiki">Contribuisci</string> <string name="hack_url">https://gitpull.it/w/librebusto/it/</string> <string name="action_source">Codice sorgente</string> <string name="action_licence">Licenza</string> <string name="action_author">Incontra l\'autore</string> <string name="added_in_favorites">Fermata aggiunta ai preferiti</string> <string name="cant_add_to_favorites">Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)!</string> <string name="title_activity_favorites">Preferiti</string> <string name="title_activity_map">Mappa</string> <string name="tip_add_favorite">Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista!</string> <string name="action_remove_from_favourites">Rimuovi</string> <string name="action_rename_bus_stop_username">Rinomina</string> <string name="dialog_rename_bus_stop_username_title">Rinomina fermata</string> <string name="dialog_rename_bus_stop_username_reset_button">Reset</string> <string name="about">Informazioni</string> <string name="howDoesItWork"><b>Tocca la stella</b> per aggiungere la fermata ai preferiti\n\n<b>Come leggere gli orari:</b> \n<b>   12:56*</b> Orario in tempo reale\n<b>   12:56</b>   Orario programmato\n\n<b>Trascina giù per aggiornare</b> l\'orario. \n<b>Tocca a lungo</b> su <b>Fonte Orari</b> per cambiare sorgente degli orari di arrivo. </string> <string name="hint_button">OK!</string> <string name="about_history"> <![CDATA[ <h1>Benvenuto!</h1> <p>Grazie per aver scelto BusTO, un\'app <b>indipendente</b> da GTT/5T, per spostarsi a Torino attraverso <b>software libero</b>:</p> <p>Perché usare BusTO?</p> <p> - Non sei <b>monitorato</b><br> - Non ci sono <b>pubblicità</b><br> - La tua <b>privacy</b> è al sicuro<br> - Inoltre l\'app è molto leggera!<br> </p> <h2>Come Funziona?</h2> <p>Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito <b>www.gtt.to.it</b> o <i>www.5t.torino.it</i> "per uso personale".</p> <p>Ingredienti:<br> - <b>Fabio Mazza</b> attuale rockstar developer anziano.<br> - <b>Andrea Ugo</b> attuale rockstar developer in formazione.<br> - <b>Silviu Chiriac</b> designer del logo 2021.<br> - <b>Marco M</b> formidabile tester e cacciatore di bug.<br> - <b>Ludovico Pavesi</b> ex rockstar developer anziano asd.<br> - <b>Valerio Bozzolan</b> attuale manutentore.<br> - <b>Marco Gagino</b> apprezzato ex collaboratore, ideatore icona e grafica.<br> - <b>JSoup</b> libreria per "<i>web scaping</i>".<br> - <b>Google</b> icone e libreria di supporto per il Material Design.<br> - Tutti i contributori! </p> <h2>Licenze</h2> <p>L\'app e il relativo codice sorgente sono distribuiti sotto la licenza <i>GNU General Public License v3+</i>. Ciò <b>significa</b> che puoi usare, studiare, migliorare e ricondividere quest\'app con <b>qualunque mezzo</b> e per <b>qualsiasi scopo</b>: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan. </p> <h2>Note</h2> <p>Quest\'applicazione è rilasciata <b>nella speranza che sia utile a tutti</b> ma senza NESSUNA garanzia.</p> <p>Buon utilizzo! :)</p> ]]> </string> <string name="query_too_short">Nome troppo corto, digita più caratteri e riprova</string> <string name="route_towards_destination">%1$s verso %2$s</string> <string name="route_towards_unknown">%s (destinazione sconosciuta)</string> <string name="internal_error">Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T</string> <string name="action_view_on_map">Visualizza sulla mappa</string> <string name="cannot_show_on_map_no_activity">Non trovo un\'applicazione dove mostrarla</string> <string name="cannot_show_on_map_no_position">Posizione della fermata non trovata</string> <string name="nearby_stops_message">Fermate vicine</string> <string name="position_searching_message">Ricerca della posizione in corso…</string> <string name="no_stops_nearby">Nessuna fermata nei dintorni</string> <string name="main_menu_pref">Preferenze</string> <string name="database_update_message">Aggiornamento del database…</string> <string name="pref_num_elements">Numero minimo di fermate</string> <string name="num_stops_nearby_not_number">Il numero di fermate da ricercare non è valido</string> <string name="invalid_number">Valore errato, inserisci un numero</string> <string name="title_activity_settings">Impostazioni</string> <string name="settings_search_radius">Distanza massima di ricerca (m)</string> <string name="settings_experimental">Funzionalità sperimentali</string> <string name="action_settings">Impostazioni</string> <string name="general_settings">Generali</string> <string name="pref_recents_group_title">Fermate recenti</string> <string name="settings_group_general">Impostazioni generali</string> <string name="settings_group_database">Gestione del database</string> <string name="settings_reset_database">Comincia aggiornamento manuale del database</string> <string name="enable_position_message_map">Consenti l\'accesso alla posizione per mostrarla sulla mappa</string> <string name="enableGpsText">Abilitare il GPS</string> <string name="bus_arriving_at">arriva alle</string> <string name="arrivals_card_at_the_stop">alla fermata</string> <string name="show_arrivals">Mostra arrivi</string> <string name="show_stops">Mostra fermate</string> <string name="nearby_arrivals_message">Arrivi qui vicino</string> <string name="removed_from_favorites">Fermata rimossa dai preferiti</string> <!-- Map view buttons strings !--> <string name="bt_center_map_description">La mia posizione</string> <string name="bt_follow_me_description">Segui posizione</string> <!-- Arrival times sources !--> <string name="times_source_fmt">Fonte orari: %1$s</string> <string name="fivetapifetcher">App GTT</string> <string name="gttjsonfetcher">Sito GTT</string> <string name="fivetscraper">Sito 5T Torino</string> + <string name="source_mato">App Muoversi a Torino</string> + <string name="arrival_source_changing">Cambiamento sorgente orari…</string> <string name="change_arrivals_source_message">Premi a lungo per cambiare la sorgente degli orari</string> <string name="default_notification_channel_description">Canale unico delle notifiche</string> <string name="too_many_permission_asks">Chiesto troppe volte per il permesso %1$s</string> <string name="permission_storage_maps_msg">Non si può usare questa funzionalità senza il permesso di archivio</string> <string name="storage_permission">di archivio</string> <string name="message_crash">Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. </string> <string name="acra_email_message">L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n</string> <string name="nav_arrivals_text">Arrivi</string> <string name="nav_map_text">Mappa</string> <string name="nav_favorites_text">Preferiti</string> <string name="drawer_open">Apri drawer</string> <string name="drawer_close">Chiudi drawer</string> <string name="experiments">Esperimenti</string> <string name="donate_now">Offrici un caffè</string> <string name="map">Mappa</string> <string name="stop_search_view_title">Ricerca fermate</string> <string name="app_version">Versione app</string> + <string name="arrival_times">Orari di arrivo</string> + + <string name="pref_directions_capitalize">Mostra direzioni in maiuscolo</string> + <string-array name="directions_capitalize"> + <item>Non cambiare</item> + <item>Tutto in maiuscolo</item> + <item>Solo la prima lettera maiuscola</item> + </string-array> </resources> diff --git a/res/values/pref_keys.xml b/res/values/pref_keys.xml index e6923a1..24e162d 100644 --- a/res/values/pref_keys.xml +++ b/res/values/pref_keys.xml @@ -1,7 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <!-- Preference screen --> <string name="pref_key_num_recents" translatable="false">pref_num_recents</string> <string name="pref_key_radius_recents" translatable="false">pref_radius_recents</string> <string name="pref_key_experimental" translatable="false">pref_exp_features</string> + + <string name="pref_arrival_times_capit" translatable="false">pref_arrival_times_capitalize</string> </resources> \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index c2735de..3ce997e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,177 +1,192 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name" translatable="false">BusTO</string> <string name="app_name_full" translatable="false">Libre BusTO</string> <string name="app_name_debug" translatable="false">BusTO dev</string> <string name="app_description">You\'re using the latest in technology when it comes to respecting your privacy. </string> <string name="search">Search</string> <string name="qrcode">Scan QR Code</string> <string name="insert_bus_stop_number">Bus stop number</string> <string name="insert_bus_stop_name">Bus stop name</string> <string name="insert_bus_stop_number_error">Insert bus stop number</string> <string name="insert_bus_stop_name_error">Insert bus stop name</string> <string name="route_towards_destination">%1$s towards %2$s</string> <string name="route_towards_unknown">%s (unknown destination)</string> <string name="network_error">Verify your Internet connection!</string> <string name="no_bus_stop_have_this_name">Seems that no bus stop have this name</string> <string name="parsing_error">Error parsing the 5T/GTT website (damn site!)</string> <string name="query_too_short">Name too short, type more characters and retry </string> <!-- TODO: carry out experiments to determine the best wording for this message and publish a paper with the findings --> <string name="passages">Arrivals at: %1$s</string> <string name="results">Choose the bus stop…</string> <string name="lines">Lines: %1$s</string> <string name="no_passages">No timetable found</string> <string name="no_qrcode">No QR code</string> <string name="internal_error">Unexpected internal error, cannot extract data from GTT/5T website</string> <string name="action_help">Help</string> <string name="action_about">About</string> <string name="action_about_more">More about</string> <string name="action_wiki">Contribute</string> <string name="hack_url">https://gitpull.it/w/librebusto/en/</string> <string name="action_source">Source code</string> <string name="action_licence">Licence</string>11 <string name="action_author">Meet the author</string> <string name="added_in_favorites">Bus stop is now in your favorites</string> <string name="removed_from_favorites">Bus stop removed from your favorites</string> <string name="action_favorites">Favorites</string> <string name="title_activity_favorites">Favorites</string> <string name="title_activity_map">Map</string> <string name="tip_add_favorite">No favorites? Arghh! Press on a bus stop star to populate this list!</string> <string name="action_remove_from_favourites">Delete</string> <string name="action_rename_bus_stop_username">Rename</string> <string name="dialog_rename_bus_stop_username_title">Rename the bus stop</string> <string name="dialog_rename_bus_stop_username_reset_button">Reset</string> <string name="about">About</string> <string name="howDoesItWork"> <b>Tap the star</b> to add the bus stop to the favourites\n\n<b>How to read timelines:</b>\n<b>   12:56*</b> Real-time arrivals\n<b>   12:56</b>   Scheduled arrivals\n\n<b>Pull down to refresh</b> the timetable \n <b>Long press on Arrivals source</b> to change the source of the arrival times </string> <string name="hint_button">GOT IT!</string> + <string name="arrival_times">Arrival times</string> <string name="about_history"> <![CDATA[ <h1>Welcome!</h1> <p>Thanks for using BusTO, a "politically" <b>independent</b> app useful to move around Torino using a <b>Free/Libre software</b>.</p> <p>Why use this app?</p> <p> - You\'ll never be <b>tracked</b><br> - You\'ll never see boring <b>ads</b><br> - We\'ll always respect your <b>privacy</b><br> - Moreover, it\'s lightweight!<br> </p> <h2>How does it work?</h2> <p>This app will show you bus timetables gathering data from <b>www.gtt.to.it</b> or <b>www.5t.torino.it</b> "for personal use".</p> <p>Who worked on BusTO:<br> - <b>Fabio Mazza</b> current senior rockstar developer.<br> - <b>Andrea Ugo</b> current junior rockstar developer.<br> - <b>Silviu Chiriac</b> designer of the 2021 logo.<br> - <b>Marco M</b> rockstar tester and bug hunter.<br> - <b>Ludovico Pavesi</b> previous senior rockstar developer asd.<br> - <b>Valerio Bozzolan</b> maintainer and infrastructure sponsor.<br> - <b>Marco Gagino</b> contributor and icon creator.<br> - <b>JSoup</b> web scraper library.<br> - <b>makovkastar</b> floating buttons.<br> - <b>Google</b> Material Design icons.<br> - All the contributors! </p> <h2>Licenses</h2> <p>The app and the related source code are released by Valerio Bozzolan under the terms of the <i>GNU General Public License v3+</i>). So everyone is allowed to use, to study, to improve and to share this app by <b>any kind of means</b> and for <b>any purpose</b>: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.</p> <h2>Notes</h2> <p>This app has been developed <b>hoping to be useful to everyone</b> but without ANY warranty.</p> <p>This translation is kindly provided by Riccardo Caniato and Marco Gagino.</p> <p>Get involved! :)</p> ]]> </string> <string name="cant_add_to_favorites">Cannot add to favorites (storage full or corrupted database?)!</string> <string name="action_view_on_map">View on a map</string> <string name="cannot_show_on_map_no_activity">Cannot find any application to show it in</string> <string name="cannot_show_on_map_no_position">Cannot find the position of the stop</string> <string name="list_fragment_debug" translatable="false">ListFragment - BusTO</string> <string name="mainSharedPreferences" translatable="false">it.reyboz.bustorino.preferences</string> <string name="databaseUpdatingPref" translatable="false">db_is_updating</string> <!-- Settings --> <string name="nearby_stops_message">Nearby stops</string> <string name="nearby_arrivals_message">Nearby connections</string> <string name="app_version">App version</string> <string name="num_stops_nearby_not_number">The number of stops to show in the recents is invalid</string> <string name="invalid_number">Invalid value, put a valid number</string> <string name="position_searching_message">Finding the position…</string> <string name="no_stops_nearby">No stops nearby</string> <string name="pref_num_elements">Minimum number of stops</string> <string name="main_menu_pref">Preferences</string> <string name="title_activity_settings">Settings</string> <string name="action_settings">Settings</string> <string name="general_settings">General</string> <string name="settings_experimental">Experimental features</string> <string name="settings_search_radius">Maximum distance (meters)</string> <string name="pref_recents_group_title">Recent stops</string> <string name="settings_group_general">General settings</string> <string name="settings_group_database">Database management</string> <string name="settings_reset_database">Launch manual database update</string> <string name="enable_position_message_map">Allow access to position to show it on the map</string> <string name="enableGpsText">Please enable GPS</string> <string name="database_update_message">Database update in progress…</string> <string name="bus_arriving_at">is arriving at</string> <string name="arrivals_card_at_the_stop">at the stop</string> <string name="two_strings_format" translatable="false">%1$s - %2$s</string> <string name="show_arrivals">Show arrivals</string> <string name="show_stops">Show stops</string> <!-- Map view buttons strings !--> <string name="bt_center_map_description">Center on my location</string> <string name="bt_follow_me_description">Follow me</string> <!-- Arrival times sources !--> <string name="times_source_fmt">Arrivals source: %1$s</string> <string name="fivetapifetcher">GTT App</string> <string name="gttjsonfetcher">GTT Website</string> <string name="fivetscraper">5T Torino website</string> + <string name="source_mato">Muoversi a Torino app</string> <string name="arrival_source_changing">Changing arrival times source…</string> <string name="change_arrivals_source_message">Long press to change the source of arrivals</string> <!-- Notifications --> <string name="default_notification_channel" translatable="false">Default</string> <string name="default_notification_channel_description">Default channel for notifications</string> <string name="too_many_permission_asks">Asked for %1$s permission too many times</string> <string name="permission_storage_maps_msg">Cannot use the map with the storage permission!</string> <string name="storage_permission">storage</string> <string name="message_crash">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. </string> <string name="acra_email_message">The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n </string> <string name="nav_arrivals_text">Arrivals</string> <string name="nav_map_text">Map</string> <string name="nav_favorites_text">Favorites</string> <string name="drawer_open">Open navigation drawer</string> <string name="drawer_close">Close navigation drawer</string> <string name="experiments">Experiments</string> <string name="donate_now">Buy us a coffee</string> <string name="map">Map</string> <string name="stop_search_view_title">Search by stop</string> + <!-- preferences --> + <string name="pref_directions_capitalize">Capitalize directions</string> + <string-array name="directions_capitalize"> + <item >Do not change arrivals directions</item> + <item>Capitalize everything</item> + <item>Capitalize only first letter</item> + </string-array> + <array name="directions_capitalize_keys"> + <item>KEEP</item> + <item>CAPITALIZE_ALL</item> + <item>CAPITALIZE_FIRST</item> + + </array> </resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index ffe8fab..a6514e0 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,52 +1,61 @@ <?xml version="1.0" encoding="utf-8"?> <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.preference.PreferenceCategory android:title="@string/general_settings"> <androidx.preference.SwitchPreferenceCompat android:defaultValue="false" android:title="@string/settings_experimental" android:key="@string/pref_key_experimental"/> </androidx.preference.PreferenceCategory> <androidx.preference.PreferenceCategory android:title="@string/pref_recents_group_title"> <!-- <androidx.preference.EditTextPreference android:defaultValue="10" android:selectAllOnFocus="true" android:singleLine="true" android:inputType="number" android:title="@string/pref_num_elements" android:key="@string/pref_key_num_recents" app:useSimpleSummaryProvider="true" android:digits="0123456789" /> --> <androidx.preference.SeekBarPreference android:title="@string/pref_num_elements" android:key="@string/pref_key_num_recents" android:defaultValue="4" android:max="40" app:min="1" android:layout_width="match_parent" android:layout_height="wrap_content" app:showSeekBarValue="true" /> <androidx.preference.SeekBarPreference android:title="@string/settings_search_radius" android:key="@string/pref_key_radius_recents" android:defaultValue="500" android:max="1000" app:min="100" android:layout_width="match_parent" android:layout_height="wrap_content" app:showSeekBarValue="true" /> </androidx.preference.PreferenceCategory> <!-- <androidx.preference.Preference android:layout="@layout/pref_extra_version" android:key="version" /> --> + <androidx.preference.PreferenceCategory android:title="@string/arrival_times"> + + <androidx.preference.ListPreference + android:title="@string/pref_directions_capitalize" + android:entries="@array/directions_capitalize" + android:entryValues="@array/directions_capitalize_keys" + android:key="@string/pref_arrival_times_capit" + /> + </androidx.preference.PreferenceCategory> </androidx.preference.PreferenceScreen> diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java index ff8a416..df56245 100644 --- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -1,147 +1,211 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.adapters; import android.content.Context; import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import android.content.SharedPreferences; +import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import java.util.List; +import java.util.Locale; +import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.utils; /** * This once was a ListView Adapter for BusLine[]. * * Thanks to Framentos developers for the guide: * http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/# * * @author Valerio Bozzolan * @author Ludovico Pavesi */ -public class PalinaAdapter extends ArrayAdapter<Route> { +public class PalinaAdapter extends ArrayAdapter<Route> implements SharedPreferences.OnSharedPreferenceChangeListener { private LayoutInflater li; private static int row_layout = R.layout.entry_bus_line_passage; private static final int metroBg = R.drawable.route_background_metro; private static final int busBg = R.drawable.route_background_bus; private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance; private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; + + private final String KEY_CAPITALIZE; + private Capitalize capit = Capitalize.DO_NOTHING; + //private static final int cityIcon = R.drawable.city; // hey look, a pattern! private static class ViewHolder { TextView rowStopIcon; TextView rowRouteDestination; TextView rowRouteTimetable; } + private static Capitalize getCapitalize(SharedPreferences shPr, String key){ + String capitalize = shPr.getString(key, ""); + + switch (capitalize.trim()){ + case "KEEP": + return Capitalize.DO_NOTHING; + case "CAPITALIZE_ALL": + return Capitalize.ALL; + + case "CAPITALIZE_FIRST": + return Capitalize.FIRST; + } + return Capitalize.DO_NOTHING; + } public PalinaAdapter(Context context, Palina p) { super(context, row_layout, p.queryAllRoutes()); li = LayoutInflater.from(context); + sort(new RouteSorterByArrivalTime()); + KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); + SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); + defSharPref.registerOnSharedPreferenceChangeListener(this); + this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE); } /** * Some parts taken from the AdapterBusLines class.<br> * Some parts inspired by these enlightening tutorials:<br> * http://www.simplesoft.it/android/guida-agli-adapter-e-le-listview-in-android.html<br> * https://www.codeofaninja.com/2013/09/android-viewholder-pattern-example.html<br> * And some other bits and bobs TIRATI FUORI DAL NULLA CON L'INTUIZIONE INTELLETTUALE PERCHÉ * SEMBRA CHE NESSUNO ABBIA LA MINIMA IDEA DI COME FUNZIONA UN ADAPTER SU ANDROID. */ @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { ViewHolder vh; if(convertView == null) { // INFLATE! // setting a parent here is not supported and causes a fatal exception, apparently. convertView = li.inflate(row_layout, null); // STORE TEXTVIEWS! vh = new ViewHolder(); vh.rowStopIcon = (TextView) convertView.findViewById(R.id.routeID); vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination); vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere); // STORE VIEWHOLDER IN\ON\OVER\UNDER\ABOVE\BESIDE THE VIEW! convertView.setTag(vh); } else { // RECOVER THIS STUFF! vh = (ViewHolder) convertView.getTag(); } Route route = getItem(position); vh.rowStopIcon.setText(route.getNameForDisplay()); if(route.destinazione==null || route.destinazione.length() == 0) { vh.rowRouteDestination.setVisibility(View.GONE); + // move around the route timetable + final ViewGroup.MarginLayoutParams pars = (ViewGroup.MarginLayoutParams) vh.rowRouteTimetable.getLayoutParams(); + if (pars!=null){ + pars.topMargin = 16; + if(Build.VERSION.SDK_INT >= 17) + pars.setMarginStart(20); + pars.leftMargin = 20; + } } else { // View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible. vh.rowRouteDestination.setVisibility(View.VISIBLE); - vh.rowRouteDestination.setText(route.destinazione); + String dest = route.destinazione; + switch (capit){ + case ALL: + dest = route.destinazione.toUpperCase(Locale.ROOT); + break; + case FIRST: + dest = utils.toTitleCase(route.destinazione, true); + break; + case DO_NOTHING: + default: + + } + vh.rowRouteDestination.setText(dest); } switch (route.type) { //UNKNOWN = BUS for the moment case UNKNOWN: case BUS: default: // convertView could contain another background, reset it vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: vh.rowStopIcon.setBackgroundResource(extraurbanoBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); vh.rowStopIcon.setBackgroundResource(metroBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case RAILWAY: vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: // never used but whatever. vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; } List<Passaggio> passaggi = route.passaggi; if(passaggi.size() == 0) { vh.rowRouteTimetable.setText(R.string.no_passages); + } else { vh.rowRouteTimetable.setText(route.getPassaggiToString()); } return convertView; } + + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(KEY_CAPITALIZE)){ + capit = getCapitalize(sharedPreferences, KEY_CAPITALIZE); + + notifyDataSetChanged(); + } + } + + enum Capitalize{ + DO_NOTHING, ALL, FIRST + } } diff --git a/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt b/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt new file mode 100644 index 0000000..d6a10f1 --- /dev/null +++ b/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt @@ -0,0 +1,29 @@ +package it.reyboz.bustorino.adapters + +import it.reyboz.bustorino.backend.Route + +class RouteSorterByArrivalTime : Comparator<Route> { + + override fun compare(route1: Route?, route2: Route?): Int { + if (route1 == null){ + if(route2 == null) return 0 + else return 2; + } else if (route2 == null){ + return -2; + } + val passaggi1 = route1.passaggi + val passaggi2 = route2.passaggi + // handle the case of midnight + if (passaggi1 == null || passaggi1.size == 0){ + if (passaggi2 == null || passaggi2.size == 0) return 0 + else return 2 + } else if (passaggi2 == null || passaggi2.size == 0){ + return -2 + } + passaggi1.sort() + passaggi2.sort() + + return passaggi1[0].compareTo(passaggi2[0]) + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java index 89d63c1..9348316 100644 --- a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java +++ b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java @@ -1,56 +1,46 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; // "arrivals" è più usato di "transit" o simili, e chi sono io per mettermi a dibattere con gli inglesi? import java.util.concurrent.atomic.AtomicReference; +/** + * Fetcher interface to describe ways to get information on arrival times + */ public interface ArrivalsFetcher extends Fetcher { -// /** -// * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. -// * Don't call this in UI thread! -// * -// * @param stopID stop ID, in normalized form. -// * @param routeID route ID, in normalized form. -// * @param res result code (will be set by this method) -// * @return arrival times -// * @see it.reyboz.bustorino.backend.Fetcher.result -// * @see FiveTNormalizer -// */ -// Palina ReadArrivalTimesRoute(String stopID, String routeID, AtomicReference<Fetcher.result> res); - /** * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. * Don't call this in UI thread! * * @param stopID stop ID, in normalized form. * @param res result code (will be set by this method) * @return arrival times * @see Result * @see FiveTNormalizer */ Palina ReadArrivalTimesAll(String stopID, AtomicReference<Result> res); /** * Get the determined source for the Fetcher * @return the source of the arrival times */ Passaggio.Source getSourceForFetcher(); } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java index b5bc5f3..093629c 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,429 +1,430 @@ /* BusTO - Backend components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import androidx.annotation.Nullable; + import android.util.Log; import it.reyboz.bustorino.data.GTTInfoInject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class FiveTAPIFetcher implements ArrivalsFetcher{ private static final String DEBUG_NAME = "FiveTAPIFetcher"; private final Map<String,String> defaultHeaders = getDefaultHeaders(); final static LinkedList<String> apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab")); @Override public Palina ReadArrivalTimesAll(String stopID, AtomicReference<Result> res) { //set the date for the request as now Palina p = new Palina(stopID); //request parameters String response = performAPIRequest(QueryType.ARRIVALS,stopID,res); if(response==null) { if(res.get()== Result.SERVER_ERROR_404) { Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the address is wrong"); res.set(Result.EMPTY_RESULT_SET); } return p; } List<Route> routes = parseArrivalsServerResponse(response, res); if(res.get()==Result.OK) { for (Route r : routes) { p.addRoute(r); } p.sortRoutes(); } return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.FiveTAPI; } List<Route> parseArrivalsServerResponse(String JSONresponse, AtomicReference<Result> res){ ArrayList<Route> routes = new ArrayList<>(3); /* Slight problem: "longName": ==> DESCRIPTION "name": "13N", "departures": [ { "arrivalTimeInt": 1272, "time": "21:12", "rt": false }] "lineType": "URBANO" ==> URBANO can be either bus or tram or METRO */ JSONArray arr; try{ arr = new JSONArray(JSONresponse); String type; Route.Type routetype = Route.Type.UNKNOWN; for(int i =0; i<arr.length();i++){ JSONObject lineJSON = arr.getJSONObject(i); String lineName=FiveTNormalizer.routeDisplayToInternal(lineJSON.getString("name")); if(lineJSON.has("lineType")) { type = lineJSON.getString("lineType"); //set the type of line if(type.equals("EXTRA")) routetype = Route.Type.LONG_DISTANCE_BUS; else routetype = Route.Type.BUS; } //Cut out the spaces in the line Name //temporary fix //lineName = lineName.replace(" ","").replace("/","B"); //TODO: parse the line description Route r = new Route(lineName,routetype,lineJSON.getString("longName")); //Log.d(DEBUG_NAME,"Creating line with name "+lineJSON.getString("name")+" and description "+lineJSON.getString("longName")); final JSONArray passagesJSON = lineJSON.getJSONArray("departures"); for(int j=0;j<passagesJSON.length();j++){ final JSONObject arrival = passagesJSON.getJSONObject(j); final String passaggio = Passaggio.createPassaggioGTT(arrival.getString("time"),arrival.getBoolean("rt")); r.addPassaggio(passaggio, Passaggio.Source.FiveTAPI); //Log.d(DEBUG_NAME,"Adding passage with time "+arrival.getString("time")+"\nrealtime="+arrival.getBoolean("rt")); } routes.add(r); } } catch (JSONException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return routes; } Collections.sort(routes); res.set(Result.OK); return routes; } public List<Route> parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{ if(response == null || response.equals("null") || response.length()==0) throw new IllegalArgumentException("Response string is null or void"); ArrayList<Route> routes = new ArrayList<>(10); JSONArray lines =new JSONArray(response); for(int i=0; i<lines.length();i++){ Route.FestiveInfo festivo = Route.FestiveInfo.UNKNOWN; final JSONObject branchJSON = lines.getJSONObject(i); final int branchid = branchJSON.getInt("branch"); String description = branchJSON.getString("description"); if(description.contains(" fittizi")){ //this means that the branch is fake continue; } String direction = branchJSON.getString("direction"); String lineName = branchJSON.getString("lineName"); Route.Type t = Route.Type.UNKNOWN; //parsing description String[] exploded = description.split(","); description = exploded[exploded.length-1]; //the real description int[] serviceDays = {}; if(exploded.length > 1) { String secondo = exploded[exploded.length-2]; if (secondo.contains("festivo")) { festivo = Route.FestiveInfo.FESTIVO; } else if (secondo.contains("feriale")) { festivo = Route.FestiveInfo.FERIALE; } else if(secondo.contains("lun. - ven")) { serviceDays = Route.reduced_week; } else if(secondo.contains("sab - fest.")){ serviceDays = Route.weekend; festivo = Route.FestiveInfo.FESTIVO; } else { /* Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+ "Couldn't find a the service days\n"+ "Description: "+secondo+","+description ); */ } if(exploded.length>2){ switch (exploded[exploded.length-3].trim()) { case "bus": t = Route.Type.BUS; break; case "tram": //never happened, but if it could happen you can get it t = Route.Type.TRAM; break; default: //nothing } } } else //only one piece if(description.contains("festivo")){ festivo = Route.FestiveInfo.FESTIVO; } else if(description.contains("feriale")){ festivo = Route.FestiveInfo.FERIALE; } if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM; //check for the presence of parenthesis String preParenthesis, postParenthesis; boolean hasParenth = false; if (description.contains("(")){ hasParenth =true; preParenthesis = description.split("\\(")[0]; postParenthesis = description.split("\\(")[1]; } else { preParenthesis = description; postParenthesis = ""; } if(preParenthesis.contains("-")){ //Sometimes the actual filtered direction still remains the full line (including both extremes) preParenthesis = preParenthesis.split("-")[1]; } final String directionFinal = hasParenth? preParenthesis.trim() + " (" + postParenthesis : preParenthesis; Route r = new Route(lineName.trim(),directionFinal.trim(),t,new ArrayList<>()); if(serviceDays.length>0) r.serviceDays = serviceDays; r.festivo = festivo; r.branchid = branchid; r.description = description.trim(); //check if we have the stop list if (branchJSON.has("branchDetail")) { final String stops = branchJSON.getJSONObject("branchDetail").getString("stops"); r.setStopsList(Arrays.asList(stops.split(","))); } routes.add(r); } return routes; } public List<Route> getDirectionsForStop(String stopID, AtomicReference<Result> res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); List<Route> routes; try{ routes = parseDirectionsFromResponse(response); res.set(Result.OK); } catch (JSONException | IllegalArgumentException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); routes = null; } return routes; } public ArrayList<Stop> getAllStopsFromGTT(AtomicReference<Result> res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); if(response==null) return null; ArrayList<Stop> stopslist; try{ //JSONObject responseJSON = new JSONObject(response); JSONArray stops = new JSONArray(response);//responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i<stops.length();i++){ JSONObject currentStop = stops.getJSONObject(i); String location = currentStop.getString("location"); if(location.trim().equals("_")) location = null; String placeName = currentStop.getString("placeName"); if(placeName.trim().equals("_")) placeName = null; String[] lines = currentStop.getString("lines").split(","); for(int l = 0; l<lines.length;l++){ lines[l] = FiveTNormalizer.routeDisplayToInternal(lines[l]); } Route.Type t; switch (currentStop.getString("type")){ case "BUS": t = Route.Type.BUS; break; case "METRO": t = Route.Type.METRO; break; case "TRENO": t = Route.Type.RAILWAY; break; default: t = Route.Type.UNKNOWN; } String stopName = currentStop.getString("name"); String stopID; if(stopName.toLowerCase().contains("metro")) t= Route.Type.METRO; try { stopID = currentStop.getString("id"); } catch (JSONException exc){ // we don't have the ID //check if we have it already as hardcoded stopID = GTTInfoInject.findIDWhenMissingByName(stopName); if(stopID.length()==0){ // we haven't found it, skip stop Log.e(DEBUG_NAME, "Cannot find the ID for stop name: "+stopName); continue; } } Stop s = new Stop(stopID, stopName, null,location,t,Arrays.asList(lines), Double.parseDouble(currentStop.getString("lat")), Double.parseDouble(currentStop.getString("lng"))); if(placeName!=null) s.setAbsurdGTTPlaceName(placeName); stopslist.add(s); } res.set(Result.OK); } catch (JSONException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return null; } return stopslist; } @Nullable public ArrayList<Route> getAllLinesFromGTT(AtomicReference<Result> res){ String resp = performAPIRequest(QueryType.LINES,null,res); if(resp==null) { return null; } ArrayList<Route> routes = null; try { JSONArray lines = new JSONArray(resp); routes = new ArrayList<>(lines.length()); for(int i = 0; i<lines.length(); i++) { JSONObject lineJ = lines.getJSONObject(i); Route.Type t; switch(lineJ.getString("azienda")){ case "EXTRA": t = Route.Type.LONG_DISTANCE_BUS; break; case "URBANO": t = Route.Type.BUS; break; case "FERRO": t = Route.Type.RAILWAY; break; default: t = Route.Type.UNKNOWN; } String name = lineJ.getString("name"); routes.add(new Route(name,t,lineJ.getString("longName"))); } //finish res.set(Result.OK); } catch (JSONException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return routes; } return routes; } /** * Useful to get all the headers for the GTT server * @return the request headers */ public static Map<String, String> getDefaultHeaders(){ HashMap<String, String> param = new HashMap<>(); param.put("Host","www.5t.torino.it"); param.put("Connection","Keep-Alive"); param.put("Accept-Encoding", "gzip"); return param; } /** * Create and perform the network request. This method adds parameters and returns the result * @param t type of request to be performed * @param stopID optional parameter, stop ID which you need for passages and branches * @param res result container * @return a String which contains the result of the query, to be parsed */ @Nullable public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference<Result> res){ URL u; Map<String,String> param; try { String address = getURLForOperation(t,stopID); //Log.d(DEBUG_NAME,"The address to query is: "+address); param = getDefaultHeaders(); u = new URL(address); } catch (UnsupportedEncodingException |MalformedURLException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return null; } String response = networkTools.queryURL(u,res,param); return response; } /** * Get the right url for the operation you are doing, to be fed into the queryURL method * @param t type of operation * @param stopID stop on which you are working on * @return the Url to go to * @throws UnsupportedEncodingException if it cannot be converted to utf-8 */ public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { final StringBuilder sb = new StringBuilder(); sb.append("http://www.5t.torino.it/ws2.1/rest/"); if(t!=QueryType.LINES) sb.append("stops/"); switch (t){ case ARRIVALS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/departures"); break; case DETAILS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/branches/details"); break; case STOPS_ALL: sb.append("all"); break; case STOPS_VERSION: sb.append("version"); break; case LINES: sb.append("lines/all"); break; } return sb.toString(); } public enum QueryType { ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 3e07757..1906619 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,377 +1,408 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; /** * Timetable for multiple routes.<br> * <br> * Apparently "palina" and a bunch of other terms can't really be translated into English.<br> * Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList<Route> routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude()); } + public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, + @Nullable String location, + @Nullable Double lat, @Nullable Double lon) { + super(ID, name, userName, location, null, null, lat, lon); + } + + public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List<String> routesThatStopHere) { + super(name, ID, location, type, routesThatStopHere); + } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { this.routes.add(new Route(routeID, destinazione, type, new ArrayList<>(6))); routesModified = true; return this.routes.size() - 1; // last inserted element and pray that direct access to ArrayList elements really is direct } public int addRoute(Route r){ this.routes.add(r); routesModified = true; return this.routes.size()-1; } public void setRoutes(List<Route> routeList){ routes = new ArrayList<>(routeList); } // /** // * Clears a route timetable (or creates an empty route) and returns its index // * // * @param routeID name // * @param destinazione end of line\terminus // * @return array index for this route // */ // public int updateRoute(String routeID, String destinazione) { // int s = this.routes.size(); // RouteInternal r; // // for(int i = 0; i < s; i++) { // r = routes.get(i); // if(r.name.compareTo(routeID) == 0 && r.destinazione.compareTo(destinazione) == 0) { // // capire se è possibile che ci siano stessa linea e stessa destinazione su 2 righe diverse del sito e qui una sovrascrive l'altra (probabilmente no) // r.updateFlag(); // r.deletePassaggio(); // return i; // } // } // // return this.addRoute(routeID, destinazione); // } // // /** // * Deletes routes marked as "not updated" (= disappeared from the GTT website\API\whatever). // * Sets all remaining routes to "not updated" because that's how this contraption works. // */ // public void finishUpdatingRoutes() { // RouteInternal r; // // for(Iterator<RouteInternal> itr = this.routes.iterator(); itr.hasNext(); ) { // r = itr.next(); // if(r.unupdateFlag()) { // itr.remove(); // } // } // } // /** // * Gets the current timetable for a route. Returns null if the route doesn't exist. // * This is slower than queryRouteByIndex. // * // * @return timetable (passaggi) // */ // public List<Passaggio> queryRoute(String routeID) { // for(Route r : this.routes) { // if(routeID.equals(r.name)) { // return r.getPassaggi(); // } // } // // return null; // } // // /** // * Gets the current timetable for this route, from its index in the array. // * // * @return timetable (passaggi) // */ // public List<Passaggio> queryRouteByIndex(int index) { // return this.routes.get(index).getPassaggi(); // } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List<Route> queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List<Route> additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List<Passaggio> passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List<Passaggio> getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList<Route> routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i<routes.size(); i++){ final Route r = routes.get(i); if(routeCheck.equals(r)){ //we have found a match, merge routes.remove(routeCheck); r.mergeRouteWithAnother(routeCheck); found=true; break; } } if (found) mergeDuplicateRoutes(startidx); else mergeDuplicateRoutes(startidx+1); } + + public int getTotalNumberOfPassages(){ + + int tot = 0; + if(routes==null) + return tot; + for(Route r: routes){ + tot += r.numPassaggi(); + } + return tot; + } + public int getMinNumberOfPassages(){ + if (routes == null) return 0; + + int min = Integer.MAX_VALUE; + if( routes.size() == 0) min = 0; + else for (Route r : routes){ + min = Math.min(min,r.numPassaggi()); + } + return min; + } //private void mergeRoute } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java index 16dcb0c..c24b0cb 100644 --- a/src/it/reyboz/bustorino/backend/Passaggio.java +++ b/src/it/reyboz/bustorino/backend/Passaggio.java @@ -1,159 +1,185 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import android.util.Log; +import java.util.Locale; + public final class Passaggio implements Comparable<Passaggio> { private static final int UNKNOWN_TIME = -3; private static final String DEBUG_TAG = "BusTO-Passaggio"; private final String passaggioGTT; public final int hh,mm; + private @Nullable Integer realtimeDifference; public final boolean isInRealTime; public final Source source; /** * Useless constructor. * * //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. */ // public Passaggio(@NonNull String TimeGTT) { // this.passaggio = TimeGTT; // } @Override public String toString() { return this.passaggioGTT; } /** * Constructs a time (passaggio) for the timetable. * * @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace. * @throws IllegalArgumentException if nothing reasonable can be extracted from the string */ public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) { passaggioGTT = TimeGTT; source = sorgente; String[] parts = TimeGTT.split(":"); String hh,mm; boolean realtime; if(parts.length != 2) { //throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!"); this.hh = UNKNOWN_TIME; this.mm = UNKNOWN_TIME; this.isInRealTime = false; return; } hh = parts[0]; if(parts[1].endsWith("*")) { mm = parts[1].substring(0, parts[1].length() - 1); realtime = true; } else { mm = parts[1]; realtime = false; } int hour=-3,min=-3; try { hour = Integer.parseInt(hh); min = Integer.parseInt(mm); } catch (NumberFormatException ex){ Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes"); hour = UNKNOWN_TIME; min = UNKNOWN_TIME; realtime = false; } finally { this.hh = hour; this.mm = min; this.isInRealTime = realtime; + } } public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){ this.hh = hour; this.mm = minutes; this.isInRealTime = realtime; + if (!realtime) realtimeDifference = 0; this.source = sorgente; //Build the passaggio string StringBuilder sb = new StringBuilder(); sb.append(hour).append(":").append(minutes); if(realtime) sb.append("*"); this.passaggioGTT = sb.toString(); } public static String createPassaggioGTT(String timeInput, boolean realtime){ final String time = timeInput.trim(); if(time.contains("*")){ if(realtime) return time; else return time.substring(0,time.length()-1); } else{ if(realtime) return time.concat("*"); else return time; } } + public Passaggio(int numSeconds, boolean realtime, int timeDiff, Source source){ + int minutes = numSeconds / 60; + int hours = minutes / 60; + //this.hh = hours; + this.mm = minutes - hours*60; + this.hh = hours % 24; + this.realtimeDifference = timeDiff/60; + this.isInRealTime = realtime; + this.source = source; + this.passaggioGTT = makePassaggioGTT(this.hh, this.mm, this.isInRealTime); + } + + private static String makePassaggioGTT(int hour, int minutes, boolean realtime){ + StringBuilder sb = new StringBuilder(); + sb.append(String.format(Locale.ITALIAN,"%02d", hour)).append(":").append(String.format(Locale.ITALIAN,"%02d", minutes)); + if(realtime) sb.append("*"); + return sb.toString(); + } @Override public int compareTo(@NonNull Passaggio other) { if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME) return 0; else { int diff = this.hh - other.hh; // an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01) if (diff > 12) { // untested diff -= 24; } else if (diff < -12) { diff += 24; } diff *= 60; diff += this.mm - other.mm; // we should take into account if one is in real time and the other isn't, shouldn't we? if (other.isInRealTime) { - ++diff; + diff+=2; } if (this.isInRealTime) { - --diff; + diff -=2; } - //TODO: separate Realtime and Non-Realtime, especially for the GTTJSONFetcher return diff; } } + + // // @Override // public String toString() { // String resultString = (this.hh).concat(":").concat(this.mm); // if(this.isInRealTime) { // return resultString.concat("*"); // } else { // return resultString; // } // } public enum Source{ - FiveTAPI,GTTJSON,FiveTScraper, UNDETERMINED + FiveTAPI,GTTJSON,FiveTScraper,MatoAPI, UNDETERMINED } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java index 70990e2..29a2b37 100644 --- a/src/it/reyboz/bustorino/backend/Route.java +++ b/src/it/reyboz/bustorino/backend/Route.java @@ -1,382 +1,407 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; public class Route implements Comparable<Route> { final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY}; final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY}; final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY}; private final static int BRANCHID_MISSING = -1; private final String name; public String destinazione; public final List<Passaggio> passaggi; //create a copy of the list, so that private List<Passaggio> sortedPassaggi; public final Type type; public String description; //ordered list of stops, from beginning to end of line private List<String> stopsList = null; public int branchid = BRANCHID_MISSING; public int[] serviceDays ={}; //0=>feriale, 1=>festivo -2=>unknown public FestiveInfo festivo = FestiveInfo.UNKNOWN; + private @Nullable String gtfsId; public enum Type { // "long distance" sono gli extraurbani. BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2); //TODO: decide to give some special parameter to each field private int code; Type(int code){ this.code = code; } public int getCode(){ return this.code; } @Nullable public static Type fromCode(int i){ switch (i){ case 1: return BUS; case 2: return LONG_DISTANCE_BUS; case 3: return METRO; case 4: return RAILWAY; case 5: return TRAM; case -2: return UNKNOWN; default: return null; } } } public enum FestiveInfo{ FESTIVO(1),FERIALE(0),UNKNOWN(-2); private int code; FestiveInfo(int code){ this.code = code; } public int getCode() { return code; } public static FestiveInfo fromCode(int i){ switch (i){ case -2: return UNKNOWN; case 0: return FERIALE; case 1: return FESTIVO; default: return UNKNOWN; } } } /** * Constructor. * * @param name route ID * @param destinazione terminus\end of line * @param type bus, long distance bus, underground, and so on * @param passaggi timetable, a good choice is an ArrayList of size 6 * @param description the description of the line, usually given by the FiveTAPIFetcher * @see Palina Palina.addRoute() method */ public Route(String name, String destinazione, List<Passaggio> passaggi, Type type, String description) { this.name = name; this.destinazione = parseDestinazione(destinazione); this.passaggi = passaggi; this.type = type; this.description = description; } /** * Constructor used in GTTJSONFetcher, see above */ public Route(String name, String destinazione, Type type, List<Passaggio> passaggi) { this(name,destinazione,passaggi,type,null); } /** * Constructor used by the FiveTAPIFetcher * @param name stop Name * @param t optional type * @param description line rough description */ public Route(String name,Type t,String description){ this(name,null,new ArrayList<>(),t,description); } /** * Exactly what it says on the tin. * * @return times from the timetable */ public List<Passaggio> getPassaggi() { return this.passaggi; } public void setStopsList(List<String> stopsList) { this.stopsList = Collections.unmodifiableList(stopsList); } public List<String> getStopsList(){ return this.stopsList; } /** * Adds a time (passaggio) to the timetable for this route * * @param TimeGTT time in GTT format (e.g. "11:22*") */ public void addPassaggio(String TimeGTT, Passaggio.Source source) { this.passaggi.add(new Passaggio(TimeGTT, source)); } //Overloaded public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) { this.passaggi.add(new Passaggio(hour, minutes, realtime, source)); } public static Route.Type getTypeFromSymbol(String route) { switch (route) { case "M": return Route.Type.METRO; case "T": return Route.Type.RAILWAY; } // default with case "B" return Route.Type.BUS; } private String parseDestinazione(String direzione){ if(direzione==null) return null; //trial to add space to the parenthesis String[] exploded = direzione.split("\\("); if(exploded.length>1){ StringBuilder sb = new StringBuilder(); sb.append(exploded[0]); for(int i=1; i<exploded.length;i++) { sb.append(" ("); sb.append(exploded[i]); } direzione = sb.toString(); } return direzione; } /** * Getter for the name * @return the name of the line */ public String getName() { return name; } public String getNameForDisplay(){ if(name.trim().equals("101Metrobus")) return "101 Metrobus"; else return name; } /** * Get all passaggi in a single string * @return the string */ public String getPassaggiToString(){ StringBuilder sb = new StringBuilder(); for(Passaggio passaggio : passaggi) { // "+" calls concat() and some other stuff internally, this should be faster //StringBuilder is THE WAY sb.append(passaggio.toString()); sb.append(" "); } return sb.toString(); } public String getPassaggiToString(int start_idx, int number, boolean sort){ StringBuilder sb = new StringBuilder(); List<Passaggio> arrivals; int max; if(sort){ if(sortedPassaggi==null){ sortedPassaggi = new ArrayList<>(passaggi.size()); sortedPassaggi.addAll(passaggi); Collections.sort(sortedPassaggi); } arrivals = sortedPassaggi; } else arrivals = passaggi; max = Math.min(start_idx + number, arrivals.size()); for(int j= start_idx; j<max;j++) { // "+" calls concat() and some other stuff internally, this should be faster //StringBuilder is THE WAY sb.append(arrivals.get(j).toString()); sb.append(" "); } return sb.toString(); } + public int numPassaggi(){ + if (passaggi==null) + return 0; + return passaggi.size(); + } + @Override public int compareTo(@NonNull Route other) { int res; int thisAsInt, otherAsInt; // sorting by numbers alone yields a far more "natural" result (36N goes before 2024, 95B next to 95, and the like) thisAsInt = networkTools.failsafeParseInt(this.name.replaceAll("[^0-9]", "")); otherAsInt = networkTools.failsafeParseInt(other.name.replaceAll("[^0-9]", "")); // compare. // numeric route IDs (names) if(thisAsInt != 0 && otherAsInt != 0) { res = thisAsInt - otherAsInt; if(res != 0) { return res; } } else { // non-numeric res = this.name.compareTo(other.name); if (res != 0) { return res; } + // compare gtfsID + if (this.gtfsId != null && other.gtfsId!=null){ + res = this.gtfsId.compareTo(other.gtfsId); + if (res!=0) return 0; + } + } // try comparing their destination if(this.destinazione!=null){ res = this.destinazione.compareTo(other.destinazione); if(res != 0) { return res; } } //compare the lines if(this.stopsList!=null && other.stopsList!=null){ int d = this.stopsList.size()-other.stopsList.size(); if(d!=0) return d; //if we are here, the two routes have the same number of stops } // probably useless, but... last attempt. if(this.type != other.type) { // ordinal() is evil or whatever, who cares. return this.type.ordinal() - other.type.ordinal(); } return 0; } + @Nullable + public String getGtfsId() { + return gtfsId; + } + + public void setGtfsId(@Nullable String gtfsId) { + this.gtfsId = gtfsId; + } + public boolean isBranchIdValid(){ return branchid!=BRANCHID_MISSING; } @Override public boolean equals(Object obj) { if(obj instanceof Route){ Route r = (Route) obj; boolean result = false; if(this.name.equals(r.name) && this.branchid == r.branchid){ if(description!=null && r.description!=null) if(!description.trim().equals(r.description.trim())) return false; + if(destinazione!=null && r.destinazione!=null){ if(!this.destinazione.trim().equals(r.destinazione.trim())) // they are not the same return false; } + if(gtfsId!=null && r.gtfsId!=null && !(gtfsId.trim().equals(r.gtfsId.trim()))) + return false; //check stops list if(this.stopsList!=null && r.stopsList!=null){ int sizeDiff = this.stopsList.size()-r.stopsList.size(); if(sizeDiff!=0) { return false; } else { //check that the stops are the same result = true; for(int j=0; j<this.stopsList.size();j++){ if(!this.stopsList.get(j).equals(r.stopsList.get(j))) { result = false; break; } } return result; } } else{ //no stopsList in one or the other return true; } } return result; } else return false; } /** * Merge informations from another route * NO CONSISTENCY CHECKS, DO BEFORE CALLING THIS METHOD * @param other the other route * @return true if there have been changes */ public boolean mergeRouteWithAnother(Route other){ boolean adjusted = false; if ((other.serviceDays!=null && this.serviceDays!=null && this.serviceDays.length==0) || (other.serviceDays!=null && this.serviceDays==null)) { this.serviceDays = other.serviceDays; adjusted = true; } if (other.getStopsList() != null && this.getStopsList() == null) this.setStopsList(other.getStopsList()); if(this.passaggi!=null && other.passaggi!=null && other.passaggi.size()>0){ this.passaggi.addAll(other.passaggi); } if(this.destinazione == null && other.destinazione!=null) { this.destinazione = other.destinazione; adjusted = true; } if(!this.isBranchIdValid() && other.isBranchIdValid()) { this.branchid = other.branchid; adjusted = true; } if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){ this.festivo = other.festivo; adjusted = true; } if(other.description!=null && (this.description==null || (this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) || (this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) { this.description = other.description; } return adjusted; } } diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java index da97419..7858aad 100644 --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -1,295 +1,298 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.reyboz.bustorino.util.LinesNameSorter; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable<Stop> { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; public @Nullable Route.Type type; private @Nullable List<String> routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; + // + public @Nullable String gtfsID = null; + /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List<String> routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * Constructor that sets EVERYTHING. */ public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List<String> routesThatStopHere, @Nullable Double lat, @Nullable Double lon) { this.ID = ID; this.name = name; this.username = userName; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildRoutesString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List<String> routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } @Nullable protected List<String> getRoutesThatStopHere(){ return routesThatStopHere; } private @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); Collections.sort(routesThatStopHere,new LinesNameSorter()); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).<br> * - empty string means "already searched everywhere, can't find it"<br> * - null means "didn't search, yet. Maybe you should try."<br> * - string means "here's the name.", obviously.<br> * * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.<br> * I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.<br> * Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } public Double getDistanceFromLocation(Location loc){ if(this.lat!=null && this.lon !=null) return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude()); else return Double.POSITIVE_INFINITY; } } diff --git a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java new file mode 100644 index 0000000..bbb7356 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -0,0 +1,109 @@ +package it.reyboz.bustorino.backend.mato; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.volley.AuthFailureError; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.HttpHeaderParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import it.reyboz.bustorino.backend.Palina; + +public class MapiArrivalRequest extends MapiVolleyRequest<Palina> { + + private final String stopName; + private final Date startingTime; + private final int timeRange, numberOfDepartures; + + public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, + int numberOfDepartures, + Response.Listener<Palina> listener, + @Nullable Response.ErrorListener errorListener) { + super(MatoAPIFetcher.QueryType.ARRIVALS, listener, errorListener); + this.stopName = stopName; + this.startingTime = startingTime; + this.timeRange = timeRange; + this.numberOfDepartures = numberOfDepartures; + } + + @Nullable + @Override + public byte[] getBody() throws AuthFailureError { + JSONObject variables = new JSONObject(); + JSONObject data = new JSONObject(); + try { + data.put("operationName","AllStopsDirect"); + variables.put("name", stopName); + variables.put("startTime", (long) startingTime.getTime()/1000); + variables.put("timeRange", timeRange); + variables.put("numberOfDepartures", numberOfDepartures); + + + data.put("variables", variables); + data.put("query", MatoAPIFetcher.QUERY_ARRIVALS); + } catch (JSONException e) { + e.printStackTrace(); + throw new AuthFailureError("Error with JSON enconding",e); + } + String requestBody = data.toString(); + Log.d("MapiArrivalBusTO", "Request variables: "+ variables); + return requestBody.getBytes(); + } + + + @Override + protected Response<Palina> parseNetworkResponse(NetworkResponse response) { + if(response.statusCode != 200) + return Response.error(new VolleyError("Response Error Code "+response.statusCode)); + final String stringResponse = new String(response.data); + Palina p = null; + + try { + JSONObject data = new JSONObject(stringResponse).getJSONObject("data"); + + JSONArray allStopsFound = data.getJSONArray("stops"); + + boolean haveManyResults = allStopsFound.length() > 1; + for (int i=0; i<allStopsFound.length(); i++){ + final JSONObject currentObj = allStopsFound.getJSONObject(i); + + p = MatoAPIFetcher.Companion.parseStopJSON(currentObj); + if (haveManyResults){ + //check we got the right one + if (p.gtfsID == null){ + continue; + } else if(p.gtfsID.contains("gtt:")){ + //valid stop + break; + } + } + + } + } catch (JSONException e) { + e.printStackTrace(); + Log.e("BusTO:MapiRequest", "Error parsing JSON: "+stringResponse); + return Response.error(new VolleyError("Error parsing the response in JSON", + e)); + } + return Response.success(p, HttpHeaderParser.parseCacheHeaders(response)); + } + + + @Nullable + @Override + protected Map<String, String> getParams() throws AuthFailureError { + return new HashMap<>(); + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java new file mode 100644 index 0000000..1e57b36 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java @@ -0,0 +1,40 @@ +package it.reyboz.bustorino.backend.mato; + +import androidx.annotation.Nullable; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Response; + +import java.util.Map; + +public abstract class MapiVolleyRequest<T> extends Request<T> { + private static final String API_URL="https://mapi.5t.torino.it/routing/v1/routers/mat/index/graphql"; + + protected final Response.Listener<T> listener; + private final MatoAPIFetcher.QueryType type; + public MapiVolleyRequest( + MatoAPIFetcher.QueryType type, + Response.Listener<T> listener, + @Nullable Response.ErrorListener errorListener) { + super(Method.POST, API_URL, errorListener); + this.type = type; + this.listener = listener; + + } + + + @Nullable + @Override + abstract protected Map<String, String> getParams() throws AuthFailureError; + + @Override + protected void deliverResponse(T response) { + listener.onResponse(response); + } + + @Override + public Map<String, String> getHeaders() throws AuthFailureError { + return MatoAPIFetcher.Companion.getREQ_PARAMETERS(); + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt new file mode 100644 index 0000000..f808952 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -0,0 +1,242 @@ +package it.reyboz.bustorino.backend.mato + +import android.content.Context +import android.util.Log +import com.android.volley.toolbox.RequestFuture +import it.reyboz.bustorino.backend.* +import org.json.JSONObject +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.TimeoutException + +import java.util.concurrent.ExecutionException + + + + +open class MatoAPIFetcher : ArrivalsFetcher { + var appContext: Context? = null + set(value) { + field = value!!.applicationContext + } + + override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference<Fetcher.Result>?): Palina { + stopID!! + val future = RequestFuture.newFuture<Palina>() + val now = Calendar.getInstance().time; + var numMinutes = 30; + var palina = Palina(stopID) + var numPassaggi = 0 + var trials = 0 + while (numPassaggi < 2 && trials < 4) { + + numMinutes += 15 + val request = MapiArrivalRequest(stopID, now, numMinutes * 60, 10, future, future) + if (appContext == null || res == null) { + Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") + return Palina(stopID) + } + val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue + request.setTag(VOLLEY_TAG) + requestQueue.add(request) + + + try { + val palinaResult = future.get(5, TimeUnit.SECONDS) + if (palinaResult!=null) { + palina = palinaResult + if (palina.totalNumberOfPassages > 0) { + res.set(Fetcher.Result.OK) + } else res.set(Fetcher.Result.EMPTY_RESULT_SET) + numPassaggi = palina.totalNumberOfPassages + } else{ + res.set(Fetcher.Result.EMPTY_RESULT_SET) + } + } catch (e: InterruptedException) { + e.printStackTrace() + res.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } + trials++ + + } + + return palina + } + + override fun getSourceForFetcher(): Passaggio.Source { + return Passaggio.Source.MatoAPI + } + + companion object{ + const val VOLLEY_TAG = "MatoAPIFetcher" + + const val DEBUG_TAG = "BusTO:MatoAPIFetcher" + + val REQ_PARAMETERS = mapOf( + "Content-Type" to "application/json; charset=utf-8", + "DNT" to "1", + "Host" to "mapi.5t.torino.it") + + fun makeRequest(type: QueryType?, variables: JSONObject) : String{ + type.let { + val requestData = JSONObject() + when (it){ + QueryType.ARRIVALS ->{ + requestData.put("operationName","AllStopsDirect") + requestData.put("variables", variables) + requestData.put("query", QUERY_ARRIVALS) + } + else -> { + //TODO all other cases + } + } + + + //todo make the request... + //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html + //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley + + } + return "" + } + fun parseStopJSON(jsonStop: JSONObject): Palina{ + val latitude = jsonStop.getDouble("lat") + val longitude = jsonStop.getDouble("lon") + val palina = Palina( + jsonStop.getString("code"), + jsonStop.getString("name"), + null, null, latitude, longitude + ) + palina.gtfsID = jsonStop.getString("gtfsId") + + val routesStoppingJSON = jsonStop.getJSONArray("routes") + val baseRoutes = mutableListOf<Route>() + for (i in 0 until routesStoppingJSON.length()){ + val routeBaseInfo = routesStoppingJSON.getJSONObject(i) + val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") + r.gtfsId = routeBaseInfo.getString("gtfsId").trim() + baseRoutes.add(r) + + } + + val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") + + for (i in 0 until routesStopTimes.length()){ + val patternJSON = routesStopTimes.getJSONObject(i) + val mRoute = parseRouteStoptimesJSON(patternJSON) + + //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") + //TODO: use directionId + palina.addRoute(mRoute) + for (r in baseRoutes) { + if (palina.gtfsID != null && r.gtfsId.equals(palina.gtfsID)) { + baseRoutes.remove(r) + break + } + } + } + for (noArrivalRoute in baseRoutes){ + palina.addRoute(noArrivalRoute) + } + //val gtfsRoutes = mutableListOf<>() + + + return palina + } + fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ + val patternJSON = jsonPatternWithStops.getJSONObject("pattern") + val routeJSON = patternJSON.getJSONObject("route"); + + val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") + val gtfsId = routeJSON.getString("gtfsId").trim() + val passages = mutableListOf<Passaggio>() + for( i in 0 until passaggiJSON.length()){ + val stoptime = passaggiJSON.getJSONObject(i) + val scheduledTime = stoptime.getInt("scheduledArrival") + val realtimeTime = stoptime.getInt("realtimeArrival") + val realtime = stoptime.getBoolean("realtime") + passages.add( + Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, + Passaggio.Source.MatoAPI) + ) + } + var routeType = Route.Type.UNKNOWN + if (gtfsId[gtfsId.length-1] == 'E') + routeType = Route.Type.LONG_DISTANCE_BUS + else when( routeJSON.getString("mode").trim()){ + "BUS" -> routeType = Route.Type.BUS + "TRAM" -> routeType = Route.Type.TRAM + } + val route = Route( + routeJSON.getString("shortName"), + patternJSON.getString("headsign"), + routeType, + passages, + ) + route.gtfsId = gtfsId + return route + } + + const val QUERY_ARRIVALS="""query AllStopsDirect( + ${'$'}name: String + ${'$'}startTime: Long + ${'$'}timeRange: Int + ${'$'}numberOfDepartures: Int + ) { + stops(name: ${'$'}name) { + __typename + lat + lon + gtfsId + code + name + desc + wheelchairBoarding + routes { + __typename + gtfsId + shortName + } + stoptimesForPatterns( + startTime: ${'$'}startTime + timeRange: ${'$'}timeRange + numberOfDepartures: ${'$'}numberOfDepartures + ) { + __typename + pattern { + __typename + headsign + directionId + route { + __typename + gtfsId + shortName + mode + } + } + stoptimes { + __typename + scheduledArrival + realtimeArrival + realtime + realtimeState + } + } + } + } + """ + } + + + enum class QueryType { + ARRIVALS, + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java index 808b629..37c3542 100644 --- a/src/it/reyboz/bustorino/backend/networkTools.java +++ b/src/it/reyboz/bustorino/backend/networkTools.java @@ -1,257 +1,262 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan 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 <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend; import android.content.Context; import androidx.annotation.Nullable; import android.util.Log; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.Map; import java.util.Scanner; import java.util.concurrent.atomic.AtomicReference; public abstract class networkTools { public static String getDOM(final URL url, final AtomicReference<Fetcher.Result> res) { //Log.d("asyncwget", "Catching URL in background: " + uri[0]); HttpURLConnection urlConnection; StringBuilder result = null; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { res.set(Fetcher.Result.SERVER_ERROR); return null; } try { InputStream in = new BufferedInputStream( urlConnection.getInputStream()); BufferedReader reader = new BufferedReader( new InputStreamReader(in)); result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line); } } catch (Exception e) { //Log.e("asyncwget", e.getMessage()); } finally { if (urlConnection != null) { urlConnection.disconnect(); } } if (result == null) { res.set(Fetcher.Result.SERVER_ERROR); return null; } res.set(Fetcher.Result.PARSER_ERROR); // will be set to "OK" later, this is a safety net in case StringBuilder returns null, the website returns an HTTP 204 or something like that. return result.toString(); } public static Fetcher.Result saveFileInCache(File outputFile, URL url) { HttpURLConnection urlConnection; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { //e.printStackTrace(); return Fetcher.Result.CONNECTION_ERROR; } urlConnection.setConnectTimeout(4000); urlConnection.setReadTimeout(50 * 1000); System.out.println("Last modified: "+new Date(urlConnection.getLastModified())); Log.d("BusTO net Tools", "Download file "+url); try (InputStream inputStream = urlConnection.getInputStream()) { //File outputFile = new File(con.getFilesDir(), fileName); //BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); FileOutputStream outputStream = new FileOutputStream(outputFile); byte buffer[] = new byte[16384]; boolean inProgress = true; while(inProgress){ int numread = inputStream.read(buffer); inProgress = (numread > 0); if(inProgress) outputStream.write(buffer, 0, numread); } outputStream.close(); //while (bufferedInputStream.available()) } catch (IOException e) { e.printStackTrace(); try { final Fetcher.Result res; if(urlConnection.getResponseCode()==404) res= Fetcher.Result.SERVER_ERROR_404; else if(urlConnection.getResponseCode()!=200) res= Fetcher.Result.SERVER_ERROR; else res= Fetcher.Result.PARSER_ERROR; urlConnection.disconnect(); return res; } catch (IOException ioException) { ioException.printStackTrace(); urlConnection.disconnect(); return Fetcher.Result.PARSER_ERROR; } } urlConnection.disconnect(); return Fetcher.Result.OK; } @Nullable public static Date checkLastModificationDate(URL url, AtomicReference<Fetcher.Result> res) { HttpURLConnection urlConnection; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { //e.printStackTrace(); res.set(Fetcher.Result.CONNECTION_ERROR); return null; } urlConnection.setConnectTimeout(4000); urlConnection.setReadTimeout(4 * 1000); System.out.println("Last modified: "+new Date(urlConnection.getLastModified())); Log.d("BusTO net Tools", "Download file "+url); final Date theDate = new Date(urlConnection.getLastModified()); try { if(urlConnection.getResponseCode()==404) res.set(Fetcher.Result.SERVER_ERROR_404); else if(urlConnection.getResponseCode()!=200) res.set(Fetcher.Result.SERVER_ERROR); } catch (IOException e) { e.printStackTrace(); res.set(Fetcher.Result.PARSER_ERROR); } urlConnection.disconnect(); //theDate.getTime() return theDate; } @Nullable static String queryURL(URL url, AtomicReference<Fetcher.Result> res){ return queryURL(url,res,null); } @Nullable static String queryURL(URL url, AtomicReference<Fetcher.Result> res, Map<String,String> headers) { HttpURLConnection urlConnection; InputStream in; String s; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch(IOException e) { //e.printStackTrace(); res.set(Fetcher.Result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY. return null; } // TODO: make this configurable? urlConnection.setConnectTimeout(3000); urlConnection.setReadTimeout(10000); if(headers!= null){ for(String key : headers.keySet()){ urlConnection.setRequestProperty(key,headers.get(key)); } } res.set(Fetcher.Result.SERVER_ERROR); // will be set to OK later try { in = urlConnection.getInputStream(); } catch (Exception e) { try { if(urlConnection.getResponseCode()==404) res.set(Fetcher.Result.SERVER_ERROR_404); } catch (IOException e2) { e2.printStackTrace(); } return null; } //s = streamToString(in); try { final long startTime = System.currentTimeMillis(); s = parseStreamToString(in); final long endtime = System.currentTimeMillis(); Log.d("NetworkTools-queryURL","reading response took "+(endtime-startTime)+" millisec"); } catch (IOException e) { e.printStackTrace(); return null; } try { in.close(); } catch(IOException ignored) { //ignored.printStackTrace(); } try { urlConnection.disconnect(); } catch(Exception ignored) { //ignored.printStackTrace(); } if(s.length() == 0) { Log.w("NET TOOLS", "string is empty"); return null; } else { //Log.d("NET TOOLS", s); return s; } } // https://stackoverflow.com/a/5445161 static String streamToString(InputStream is) { Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } /** * New method, maybe faster, to read inputStream * also see https://stackoverflow.com/a/5445161 * @param is what to read * @return the String Read * @throws IOException from the InputStreamReader */ static String parseStreamToString(InputStream is) throws IOException{ final int bufferSize = 1024; final char[] buffer = new char[bufferSize]; final StringBuilder out = new StringBuilder(); InputStreamReader in = new InputStreamReader(is, "UTF-8"); int rsz= in.read(buffer, 0, buffer.length); while( rsz >0) { out.append(buffer, 0, rsz); rsz = in.read(buffer, 0, buffer.length); } return out.toString(); } + /** + * Parses string into int, return 0 if it fails + * @param str the string to parse + * @return the value as int, 0 if it fails + */ static int failsafeParseInt(String str) { try { return Integer.parseInt(str); } catch(NumberFormatException e) { return 0; } } } diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index 2e44bfb..2d615b9 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,171 +1,183 @@ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.List; +import java.util.Locale; + +import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } /* public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } */ public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } - public static String toTitleCase(String givenString) { + public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.split(" "); - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (int i = 0; i < arr.length; i++) { - if (arr[i].length() > 1) - sb.append(Character.toUpperCase(arr[i].charAt(0))) - .append(arr[i].substring(1)).append(" "); + if (arr[i].length() > 1) { + sb.append(Character.toUpperCase(arr[i].charAt(0))); + if (lowercaseRest) + sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); + else + sb.append(arr[i].substring(1)); + sb.append(" "); + } else sb.append(arr[i]); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } } + public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ + return new ArrivalsFetcher[]{ new MatoAPIFetcher(), + new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + } /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List<String> dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i<dat.size(); i++){ sb.append(separator); sb.append(dat.get(i)); } return sb.toString(); } } diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java index 206661d..6eba1d3 100644 --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,512 +1,516 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.fragments; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class ArrivalsFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks<Cursor> { private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment"; private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; - private final static ArrivalsFetcher[] defaultFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; static final String STOP_TITLE = "messageExtra"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; private boolean fetchersChangeRequestPending = false; private boolean stopIsInFavorites = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; - private List<ArrivalsFetcher> fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); + private List<ArrivalsFetcher> fetchers = new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); } public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); //parameter for ResultListFragmentrequestArrivalsForStopID args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null){ args.putString(KEY_STOP_NAME,stopName); } fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID; //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); resultsListView = (ListView) root.findViewById(R.id.resultsListView); timesSourceTextView = (TextView) root.findViewById(R.id.timesSourceTextView); timesSourceTextView.setOnLongClickListener(view -> { if(!fetchersChangeRequestPending){ rotateFetchers(); //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing); mListener.requestArrivalsForStopID(stopID); fetchersChangeRequestPending = true; return true; } return false; }); timesSourceTextView.setOnClickListener(view -> { Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT) .show(); }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites toggleLastStopToFavorites(); }); resultsListView.setOnItemClickListener((parent, view, position, id) -> { String routeName; Route r = (Route) parent.getItemAtPosition(position); routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay()); if (routeName == null) { routeName = r.getNameForDisplay(); } if (r.destinazione == null || r.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show(); } }); String displayName = getArguments().getString(STOP_TITLE); if(displayName!=null) setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated); /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ if(stopID!=null){ //refresh the arrivals if(!justCreated){ if (reloadOnResume) mListener.requestArrivalsForStopID(stopID); } else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { Log.d(DEBUG_TAG, "Restarting loader for stop"); loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach = false; } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders()); loaderManager.destroyLoader(loaderFavId); } @Nullable public String getStopID() { return stopID; } public boolean reloadsOnResume() { return reloadOnResume; } public void setReloadOnResume(boolean reloadOnResume) { this.reloadOnResume = reloadOnResume; } /** * Give the fetchers * @return the list of the fetchers */ public ArrayList<Fetcher> getCurrentFetchers(){ ArrayList<Fetcher> v = new ArrayList<Fetcher>(); for (ArrivalsFetcher fetcher: fetchers){ v.add(fetcher); } return v; } public Fetcher[] getCurrentFetchersAsArray(){ Fetcher[] arr = new Fetcher[fetchers.size()]; fetchers.toArray(arr); return arr; } private void rotateFetchers(){ Collections.rotate(fetchers, -1); } /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ if (p!=null) lastUpdatedPalina = p; if (!isAdded()){ //defer update at next show if (p==null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null"); else needUpdateOnAttach = true; } else { final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina); showArrivalsSources(lastUpdatedPalina); super.resetListAdapter(adapter); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); if (source == null){ Log.e(DEBUG_TAG, "NULL SOURCE"); return; } String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; + case MatoAPI: + source_txt = getString(R.string.source_mato); + break; case UNDETERMINED: //Don't show the view - timesSourceTextView.setVisibility(View.GONE); - return; + source_txt = ""; + break; default: throw new IllegalStateException("Unexpected value: " + source); } int count = 0; + if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 100){ //we need to update the fetcher that is requested rotateFetchers(); count++; } if (count>10) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work"); final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setVisibility(View.VISIBLE); timesSourceTextView.setText(base_message); fetchersChangeRequestPending = false; } @Override public void setNewListAdapter(ListAdapter adapter) { throw new UnsupportedOperationException(); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && stopName.length() > 0) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } @NonNull @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ // IT'S IN FAVORITES data.moveToFirst(); final String probableName = data.getString(colUserName); stopIsInFavorites = true; stopName = probableName; //update the message in the textview updateMessage(); } else { stopIsInFavorites =false; } updateStarIcon(); if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); stopName = data.getString(data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME )); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onLoaderReset(Loader<Cursor> loader) { //NOTHING TO DO } public void toggleLastStopToFavorites() { Stop stop = lastUpdatedPalina; if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE, v->updateStarIconFromLastBusStop(v)).execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true); } } /** * Update the star "Add to favorite" icon */ public void updateStarIconFromLastBusStop(Boolean toggleDone) { if (stopIsInFavorites) stopIsInFavorites = !toggleDone; else stopIsInFavorites = toggleDone; updateStarIcon(); // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ public void updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } } } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index 6f76a9c..4d7a8f9 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,724 +1,725 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; 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.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.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.zxing.integration.android.IntentIntegrator; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncDataDownload; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{ private static final String OPTION_SHOW_LEGEND = "show_legend"; private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private boolean setupOnAttached = 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; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; - private final ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + private final ArrivalsFetcher[] arrivalsFetchers = utils.getDefaultArrivalsFetchers(); //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; 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()){ //request Stops pendingNearbyStopsRequest = false; if (getContext()!= null) mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @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){ pendingNearbyStopsRequest = false; mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @Override public void onLocationDisabled() { } }; private final ActivityResultLauncher<String[]> requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { @Override public void onActivityResult(Map<String, Boolean> result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) ) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && 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); } } } }); //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { MainScreenFragment fragment = new MainScreenFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @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); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.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(getContext()); 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, "Setup on attached: "+setupOnAttached); //Restore instance state if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnAttached = false; } } if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); 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(); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); + Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! mainHandler.post(new NearbyStopsRequester(context, cr)); else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnAttached = false; } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called"); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } else if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //we have already asked for the location, and we should show an explanation in order // to ask again (TODO) //do nothing } else{ //request permission requestPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS); } } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // 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); 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); } suppressArrivalsReload = false; } if(pendingStopID!=null){ 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(); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); 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()< 3){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query); } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * 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()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } @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 prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } void showNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = fragMan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = fragMan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); ft.commit(); } @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) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @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); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } 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() new AsyncDataDownload(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { new AsyncDataDownload(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } /////////// 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/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java index 931c9f0..afc4d83 100644 --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,144 +1,146 @@ /* BusTO - Fragments components Copyright (C) 2020 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.*; import it.reyboz.bustorino.R; import java.lang.ref.WeakReference; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ + //ListPreference preference = findPreference(R.string.arrival_times) + } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); //sometimes this happens if(getContext()==null) return; /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(getContext()); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference<DialogPreference> preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index bac7eab..12d2c97 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,382 +1,390 @@ /* 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 <http://www.gnu.org/licenses/>. */ 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.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; 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.Collections; 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 AsyncDataDownload extends AsyncTask<String, Fetcher.Result,Object>{ private static final String TAG = "BusTO-DataDownload"; private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference<Fetcher.Result> res; private final RequestType t; private String query; WeakReference<FragmentHelper> helperRef; private final ArrayList<Thread> otherActivities = new ArrayList<>(); private final Fetcher[] theFetchers; - private Context context; + @SuppressLint("StaticFieldLeak") + private final Context context; private final boolean replaceFragment; public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) { RequestType type; helperRef = new WeakReference<>(fh); fh.setLastTaskRef(new WeakReference<>(this)); res = 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!"); } if (theFetchers[0] instanceof ArrivalsFetcher){ type = RequestType.ARRIVALS; } else if (theFetchers[0] instanceof StopsFinderByName){ type = RequestType.STOPS; } else{ type = null; } t = type; } @Override protected Object doInBackground(String... params) { RecursionHelper<Fetcher> r = new RecursionHelper<>(theFetchers); boolean success=false; Object result; FragmentHelper fh = helperRef.get(); ArrayList<Fetcher.Result> results = new ArrayList<>(theFetchers.length); //If the FragmentHelper is null, that means the activity doesn't exist anymore 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 switch (t){ case ARRIVALS: ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); + if (f instanceof MatoAPIFetcher){ + ((MatoAPIFetcher)f).setAppContext(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,res); - publishProgress(res.get()); + //if (res.get()!= Fetcher.Result.OK) Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+res.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference<Fetcher.Result> gres = new AtomicReference<>(); List<Route> 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); } //put updated values into Database } if(lastSearchedBusStop != null && res.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(p.queryAllRoutes().size() == 0) - //skip the rest and go to the next fetcher - continue; + if (p.getTotalNumberOfPassages() == 0) + res.set(Fetcher.Result.EMPTY_RESULT_SET); + publishProgress(res.get()); + //p.sortRoutes(); result = p; + //TODO: find a way to avoid overloading the user with toasts break; case STOPS: StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward(); List<Stop> resultList= finder.FindByName(params[0], this.res); //it's a List<Stop> Log.d(TAG,"Using the StopFinderByName: "+finder.getClass()); query =params[0]; result = resultList; //dummy result Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultList.size()+" stops"); break; default: result = null; } //find if it went well results.add(res.get()); if(res.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return result; } } boolean emptyResults = true; for (Fetcher.Result re: results){ if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { emptyResults = false; break; } } if(emptyResults){ if(t==RequestType.STOPS) publishProgress(Fetcher.Result.EMPTY_RESULT_SET); } //at this point, we are sure that the result has been negative failedAll=true; return null; } @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); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Object o) { FragmentHelper fh = helperRef.get(); if(failedAll || o == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; switch (t){ case ARRIVALS: Palina palina = (Palina) o; fh.createOrUpdateStopFragment(palina, replaceFragment); break; case STOPS: //this should never be a problem if(!(o instanceof List<?>)){ throw new IllegalStateException(); } List<?> list = (List<?>) o; if (list.size() ==0) return; Object firstItem = list.get(0); if(!(firstItem instanceof Stop)) return; ArrayList<Stop> stops = new ArrayList<>(); for(Object x: list){ if(x instanceof Stop) stops.add((Stop) x); //Log.d(DEBUG_TAG, "Parsing Stop: "+x); } if(list.size() != stops.size()){ Log.w(DEBUG_TAG, "Wrong stop list size:\n incoming: "+ list.size()+" out: "+stops.size()); } //List<Stop> stopList = (List<Stop>) list; if(query!=null && !isCancelled()) { fh.createStopListFragment(stops,query, replaceFragment); } else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT"); break; case DBUPDATE: break; } } @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 enum RequestType { ARRIVALS,STOPS,DBUPDATE } public static class BranchInserter implements Runnable{ private final List<Route> routesToInsert; private final Context context; //private final NextGenDB nextGenDB; public BranchInserter(List<Route> routesToInsert,@NonNull Context con) { this.routesToInsert = routesToInsert; this.context = con.getApplicationContext(); //nextGenDB = new NextGenDB(context); } @Override public void run() { final NextGenDB nextGenDB = new NextGenDB(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList<ContentValues> branchesValues = new ArrayList<>(routesToInsert.size()*4); ArrayList<ContentValues> connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); //values[routesToInsert.indexOf(r)] = cv; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i<r.getStopsList().size();i++){ String stop = r.getStopsList().get(i); final ContentValues connVal = new ContentValues(); connVal.put(ConnectionsTable.COLUMN_STOP_ID,stop); connVal.put(ConnectionsTable.COLUMN_ORDER,i); connVal.put(ConnectionsTable.COLUMN_BRANCH,r.branchid); //add to global connVals connectionsVals.add(connVal); } } starttime = System.currentTimeMillis(); ContentResolver cr = context.getContentResolver(); try { cr.bulkInsert(Uri.parse("content://" + AppDataProvider.AUTHORITY + "/branches/"), branchesValues.toArray(new ContentValues[0])); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted branches, took " + (endtime - starttime) + " ms"); } catch (SQLException exc){ Log.e("AsyncDataDownload","Inserting data: some error happened, aborting the database insert"); exc.printStackTrace(); return; } if (connectionsVals.size()>0) { starttime = System.currentTimeMillis(); ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); } nextGenDB.close(); } } }