diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6bd7566..5291345 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,116 +1,127 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="it.reyboz.bustorino"> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> <activity android:name=".ActivityMain" android:label="@string/app_name" android:screenOrientation="portrait" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:host="www.gtt.to.it" android:pathPrefix="/cms/percorari/arrivi" android:scheme="http"/> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:host="gtt.to.it" android:pathPrefix="/cms/percorari/arrivi" android:scheme="http"/> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:host="m.gtt.to.it" android:pathPrefix="/m/it/arrivi.jsp" android:scheme="http"/> </intent-filter> </activity> <activity android:name=".ActivityAbout" android:label="@string/about" android:parentActivityName=".ActivityMain" android:theme="@style/AboutTheme"> <!-- API < 16: --> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".ActivityMain"/> </activity> <activity android:name=".ActivityFavorites" android:label="@string/title_activity_favorites" android:parentActivityName=".ActivityMain" android:theme="@style/FavTheme"> <!-- API < 16: --> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".ActivityMain"/> </activity> + <activity + android:name=".ActivityMap" + android:label="@string/title_activity_map" + android:parentActivityName=".ActivityMain" + android:theme="@style/MapTheme"> + + <!-- API < 16: --> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".ActivityMain"/> + </activity> <provider android:name=".middleware.AppDataProvider" android:authorities="it.reyboz.bustorino.provider" android:enabled="true" android:exported="false"> </provider> <service android:name=".middleware.DatabaseUpdateService" android:exported="false"> </service> <!-- Don't show the additional frame on samsung phones --> <meta-data android:name="com.samsung.android.icon_container.has_icon_container" android:value="true"/> <activity android:name=".ActivitySettings" android:label="@string/title_activity_settings" android:parentActivityName=".ActivityMain" android:theme="@style/AppTheme"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="it.reyboz.bustorino.ActivityMain"/> </activity> </application> </manifest> diff --git a/build.gradle b/build.gradle index 2f5691c..f3436fa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,85 +1,87 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } dependencies { classpath 'com.android.tools.build:gradle:3.1.0' } ext { support_ver = "25.4.0" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() } } apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion '27.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 9 targetSdkVersion 25 versionCode 28 versionName "1.12" vectorDrawables.useSupportLibrary = 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 { implementation "com.android.support:support-v4:$support_ver" implementation "com.android.support:appcompat-v7:$support_ver" implementation "com.android.support:design:$support_ver" implementation "com.android.support:recyclerview-v7:$support_ver" implementation "com.android.support:preference-v7:$support_ver" implementation "com.android.support:cardview-v7:$support_ver" implementation 'org.jsoup:jsoup:1.11.3' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.1.1' + + implementation 'org.osmdroid:osmdroid-android:6.1.3' } } diff --git a/res/drawable/bus_marker.xml b/res/drawable/bus_marker.xml new file mode 100644 index 0000000..21d020d --- /dev/null +++ b/res/drawable/bus_marker.xml @@ -0,0 +1,12 @@ +<!-- drawable/bus_marker.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:strokeWidth="1" + android:strokeColor="#FFF" + android:fillColor="@color/metro_red" + android:pathData="M12 2C7.58 2 4 2.5 4 6V16A3 3 0 0 0 5 18.22V20A1 1 0 0 0 6 21H7A1 1 0 0 0 8 20V19H14A8 8 0 0 1 13 15.5A5.55 5.55 0 0 1 15.38 11H6V6H18V10A4.07 4.07 0 0 1 18.5 10A5.34 5.34 0 0 1 20 10.22V6C20 2.5 16.42 2 12 2M7.5 14A1.5 1.5 0 1 1 6 15.5A1.5 1.5 0 0 1 7.5 14M18.5 12A3.54 3.54 0 0 0 15 15.5C15 18.1 18.5 22 18.5 22S22 18.1 22 15.5A3.54 3.54 0 0 0 18.5 12M18.5 16.8A1.2 1.2 0 1 1 18.5 14.4A1.29 1.29 0 0 1 19.7 15.6A1.15 1.15 0 0 1 18.5 16.8Z" /> +</vector> \ No newline at end of file diff --git a/res/drawable/map.xml b/res/drawable/map.xml new file mode 100644 index 0000000..38b32b0 --- /dev/null +++ b/res/drawable/map.xml @@ -0,0 +1,8 @@ +<!-- drawable/map.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#FFF" android:pathData="M15,19L9,16.89V5L15,7.11M20.5,3C20.44,3 20.39,3 20.34,3L15,5.1L9,3L3.36,4.9C3.15,4.97 3,5.15 3,5.38V20.5A0.5,0.5 0 0,0 3.5,21C3.55,21 3.61,21 3.66,20.97L9,18.9L15,21L20.64,19.1C20.85,19 21,18.85 21,18.62V3.5A0.5,0.5 0 0,0 20.5,3Z" /> +</vector> diff --git a/res/layout/activity_map.xml b/res/layout/activity_map.xml new file mode 100644 index 0000000..bd7bbc6 --- /dev/null +++ b/res/layout/activity_map.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + <org.osmdroid.views.MapView android:id="@+id/map" + android:layout_width="fill_parent" + android:layout_height="fill_parent" /> +</LinearLayout> \ No newline at end of file diff --git a/res/layout/map_popup.xml b/res/layout/map_popup.xml new file mode 100644 index 0000000..76a33d8 --- /dev/null +++ b/res/layout/map_popup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@color/cardview_light_background"> + + <TextView + android:id="@+id/bubble_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/blue_500" /> + + <TextView + android:id="@+id/bubble_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/bubble_subdescription" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <ImageView + android:id="@+id/bubble_image" + android:layout_height="wrap_content" + android:layout_width="wrap_content" /> + +</LinearLayout> \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml index fdacb5c..bb8fa22 100644 --- a/res/menu/main.xml +++ b/res/menu/main.xml @@ -1,52 +1,57 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="it.reyboz.bustorino.ActivityMain"> - + <item + android:id="@+id/action_map" + android:icon="@drawable/map" + android:orderInCategory="1" + android:title="map" + app:showAsAction="ifRoom" /> <!--suppress AndroidDomInspection --> <!-- Android Studio can't find ic_star even though it's there. --> <item android:id="@+id/action_favorites" android:icon="@drawable/ic_star" android:orderInCategory="1" android:title="@string/action_favorites" app:showAsAction="ifRoom" /> <item android:orderInCategory="2" android:title="@string/action_about"> <menu> <item android:id="@+id/action_about" android:orderInCategory="4" android:title="@string/action_about_more" /> <item android:id="@+id/action_news" android:orderInCategory="5" android:title="@string/action_news" /> <item android:id="@+id/action_bugs" android:orderInCategory="6" android:title="@string/action_bugs" app:showAsAction="never" /> <item android:id="@+id/action_source" android:orderInCategory="7" android:title="@string/action_source" app:showAsAction="never" /> <item android:id="@+id/action_licence" android:orderInCategory="8" android:title="@string/action_licence" app:showAsAction="never" /> </menu> </item> <item android:id="@+id/action_settings" android:orderInCategory="3" android:title="@string/action_settings" android:visible="true" /> <item android:id="@+id/action_help" android:orderInCategory="4" android:title="@string/action_help" android:visible="true" /> </menu> \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index f05e2ab..3623f2b 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,111 +1,113 @@ <?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_news">News</string> <string name="action_bugs">Invia bug</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! Schiaccia sulla stella di una fermata per aggiungere 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.</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.<br> - - <b>Ludovico Pavesi</b> ex rockstar developer.<br> + - <b>Fabio Mazza</b> attuale rockstar developer anziano.<br> + - <b>Andrea Ugo</b> attuale rockstar developer in formazione.<br> + - <b>Ludovico Pavesi</b> ex rockstar developer anziano.<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 di fermate</string> <string name="title_activity_settings">Impostazioni</string> <string name="action_settings">Impostazioni</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="enableGpsText">Abilitare il GPS</string> <string name="settings_search_radius">Raggio di ricerca</string> <string name="settings_experimental">Funzionalità sperimentali</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> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 338cbb3..f60e101 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,120 +1,122 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name" translatable="false">BusTO</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_news">News releases</string> <string name="action_bugs">Bug submission</string> <string name="action_source">Source code</string> <string name="action_licence">Licence</string> <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 </string> <string name="hint_button">GOT IT!</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 rockstar developer.<br> - - <b>Ludovico Pavesi</b> previous rockstar developer.<br> - - <b>Valerio Bozzolan</b> maintainer.<br> - - <b>Marco Gagino</b> ex contributor and icon creator.<br> + - <b>Fabio Mazza</b> current senior rockstar developer.<br> + - <b>Andrea Ugo</b> current junior rockstar developer.<br> + - <b>Ludovico Pavesi</b> previous senior rockstar developer.<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> <string name="nearby_stops_message">Nearby stops</string> <string name="nearby_arrivals_message">Nearby connections</string> <string name="position_searching_message">Finding the position…</string> <string name="no_stops_nearby">No stops nearby</string> <string name="pref_num_elements">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="settings_experimental">Experimental features</string> <string name="settings_search_radius">Search radius</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="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> </resources> diff --git a/res/values/theme.xml b/res/values/theme.xml index 9766bd6..da27fe5 100644 --- a/res/values/theme.xml +++ b/res/values/theme.xml @@ -1,18 +1,24 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <style name="FavTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/blue_500</item> <item name="colorPrimaryDark">@color/blue_700</item> <item name="colorAccent">@color/teal_500</item> </style> <style name="AboutTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/teal_300</item> <item name="colorPrimaryDark">@color/teal_500</item> <item name="colorAccent">@color/blue_700</item> </style> + <style name="MapTheme" parent="Theme.AppCompat.Light.DarkActionBar"> + <item name="colorPrimary">@color/orange_500</item> + <item name="colorPrimaryDark">@color/orange_700</item> + <item name="colorAccent">@color/teal_500</item> + </style> + <style name="preferenceTheme" parent="PreferenceThemeOverlay"></style> </resources> \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityFavorites.java b/src/it/reyboz/bustorino/ActivityFavorites.java index 75ca998..2587bd7 100644 --- a/src/it/reyboz/bustorino/ActivityFavorites.java +++ b/src/it/reyboz/bustorino/ActivityFavorites.java @@ -1,304 +1,310 @@ /* 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; import android.database.Cursor; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.widget.*; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.adapters.StopAdapter; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; import it.reyboz.bustorino.middleware.StopsDB; import it.reyboz.bustorino.middleware.UserDB; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; import android.os.AsyncTask; import android.support.v4.app.NavUtils; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; +import org.osmdroid.util.GeoPoint; + import java.util.List; public class ActivityFavorites extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> { private ListView favoriteListView; private SQLiteDatabase userDB; private EditText bus_stop_name; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_favorites); // this should be done in onStarted and closed in onStop, but apparently onStarted is never run. this.userDB = new UserDB(getApplicationContext()).getWritableDatabase(); ActionBar ab = getSupportActionBar(); assert ab != null; ab.setIcon(R.drawable.ic_launcher); ab.setDisplayHomeAsUpEnabled(true); // Back button favoriteListView = (ListView) findViewById(R.id.favoriteListView); createFavoriteList(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (v.getId() == R.id.favoriteListView) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { // Respond to the action bar's Up/Home button case android.R.id.home: NavUtils.navigateUpFromSameTask(this); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item .getMenuInfo(); Stop busStop = (Stop) favoriteListView.getItemAtPosition(info.position); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: // remove the stop from the favorites in background new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE) { /** * Callback fired when everything was done * * @param result */ @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); // update the favorite list createFavoriteList(); } }.execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: final String theGeoUrl = busStop.getGeoURL(); if(theGeoUrl==null){ //doesn't have a position Toast.makeText(getApplicationContext(),R.string.cannot_show_on_map_no_position,Toast.LENGTH_SHORT).show(); return true; } - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(theGeoUrl)); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if(intent.resolveActivity(getPackageManager())!=null) - startActivity(intent); - else { - Toast.makeText(getApplicationContext(),R.string.cannot_show_on_map_no_activity,Toast.LENGTH_SHORT).show(); - } + + // start ActivityMap with these extras in intent + Intent intent = new Intent(ActivityFavorites.this, ActivityMap.class); + Bundle b = new Bundle(); + b.putDouble("lat", busStop.getLatitude()); + b.putDouble("lon", busStop.getLongitude()); + b.putString("name", busStop.getStopDefaultName()); + b.putString("ID", busStop.ID); + intent.putExtras(b); + + startActivity(intent); return true; default: return super.onContextItemSelected(item); } } void createFavoriteList() { // TODO: memoize default list, query only user names every time? new AsyncGetFavorites(getApplicationContext(), this.userDB).execute(); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); bus_stop_name = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); bus_stop_name.setText(busStop.getStopDisplayName()); bus_stop_name.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String busStopUsername = bus_stop_name.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.length() == 0) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); UserDB.updateStop(busStop, userDB); createFavoriteList(); } } else { // changed to something // something different? if(oldUserName == null || !busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); UserDB.updateStop(busStop, userDB); createFavoriteList(); } } } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // delete user name from database busStop.setStopUserName(null); UserDB.updateStop(busStop, userDB); createFavoriteList(); } }); builder.show(); } /** * This one runs. onStart instead gets ignored for no reason whatsoever. * * @see <a href="https://i.stack.imgur.com/SAX9I.png">Android Activity Lifecycle</a> */ @Override protected void onStop() { super.onStop(); this.userDB.close(); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { } @Override public void onLoaderReset(Loader<Cursor> loader) { } private class AsyncGetFavorites extends AsyncTask<Void, Void, List<Stop>> { private Context c; private SQLiteDatabase userDB; AsyncGetFavorites(Context c, SQLiteDatabase userDB) { this.c = c; this.userDB = userDB; } @Override protected List<Stop> doInBackground(Void... voids) { StopsDB stopsDB = new StopsDB(c); stopsDB.openIfNeeded(); List<Stop> busStops = UserDB.getFavorites(this.userDB, stopsDB); stopsDB.closeIfNeeded(); return busStops; } @Override protected void onPostExecute(List<Stop> busStops) { // If no data is found show a friendly message if (busStops.size() == 0) { favoriteListView.setVisibility(View.INVISIBLE); TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * redrwaing everything. */ // Show results favoriteListView.setAdapter(new StopAdapter(this.c, busStops)); favoriteListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { /** * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ Stop busStop = (Stop) parent.getItemAtPosition(position); Intent intent = new Intent(ActivityFavorites.this, ActivityMain.class); Bundle b = new Bundle(); // TODO: is passing a serialized object a good idea? Or rather, is it reasonably fast? //b.putSerializable("bus-stop-serialized", busStop); b.putString("bus-stop-ID", busStop.ID); b.putString("bus-stop-display-name", busStop.getStopDisplayName()); intent.putExtras(b); //intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); // Intent.FLAG_ACTIVITY_CLEAR_TASK isn't supported in API < 11 and we're targeting API 7... intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); startActivity(intent); finish(); } }); registerForContextMenu(favoriteListView); } } } diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 5bccbaf..3ba0a3d 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,903 +1,906 @@ /* 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; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.NavUtils; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.*; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import android.support.design.widget.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; import java.util.List; public class ActivityMain extends GeneralActivity implements FragmentListener { /* * Layout elements */ private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private SwipeRefreshLayout swipeRefreshLayout; private FloatingActionButton floatingActionButton; private FragmentManager framan; 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; /* * Options */ private final String OPTION_SHOW_LEGEND = "show_legend"; private final String LOCATION_PERMISSION_GIVEN = "loc_permission"; /* * Status */ private DBStatusManager prefsManager; private DBStatusManager.OnDBUpdateStatusChangeListener updatelistener; private static final String DEBUG_TAG = "BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference<result> res) { SystemClock.sleep(5000); res.set(result.SERVER_ERROR); return new Palina(); } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ private RecursionHelper<ArrivalsFetcher> ArrivalFetchersRecursionHelper = new RecursionHelper<>(new ArrivalsFetcher[]{new GTTJSONFetcher(), new FiveTScraperFetcher()}); private RecursionHelper<StopsFinderByName> StopsFindersByNameRecursionHelper = new RecursionHelper<>(new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); /* * Position */ //Fine location criteria private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; /* * Database Access */ private StopsDB stopsDB; private UserDB userDB; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// /* * @see swipeRefreshLayout */ private Handler handler = new Handler(); private final Runnable refreshing = new Runnable() { public void run() { if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); String stopName = fragment.getStopID(); new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(stopName); } else new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(); } }; //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); this.stopsDB = new StopsDB(getApplicationContext()); this.userDB = new UserDB(getApplicationContext()); setContentView(R.layout.activity_main); busStopSearchByIDEditText = (EditText) findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = (EditText) findViewById(R.id.busStopSearchByNameEditText); progressBar = (ProgressBar) findViewById(R.id.progressBar); howDoesItWorkTextView = (TextView) findViewById(R.id.howDoesItWorkTextView); hideHintButton = (Button) findViewById(R.id.hideHintButton); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.listRefreshLayout); floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton); framan.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { @Override public void onBackStackChanged() { Log.d("MainActivity, BusTO", "BACK STACK CHANGED"); } }); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); busStopSearchByNameEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); // Called when the layout is pulled down swipeRefreshLayout .setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { handler.post(refreshing); } }); /** * @author Marco Gagino!!! */ //swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); fh = new FragmentHelper(this, R.id.listRefreshLayout, R.id.resultFrame); setSearchModeBusStopID(); //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; String busStopDisplayName = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); busStopDisplayName = b.getString("bus-stop-display-name"); /** * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning ArrivalFetchersRecursionHelper.reset(); Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully setBusStopSearchByIDEditText(busStopID); /* //THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS // STOP IS ADDED AUTOMATICALLY Stop nextStop = new Stop(busStopID); // forcing it as user name even though it could be standard name, it doesn't really matter nextStop.setStopUserName(busStopDisplayName); //set stop as last succe fh.setLastSuccessfullySearchedBusStop(nextStop); */ createFragmentForStop(busStopID); } //Try (hopefully) database update //TODO: Start the service in foreground, check last time it ran before DatabaseUpdateService.startDBUpdate(getApplicationContext()); /* Set database update */ updatelistener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public boolean defaultStatusValue() { return true; } @Override public void onDBStatusChanged(boolean updating) { if (updating) { createDefaultSnackbar(); } else if (snackbar != null) { snackbar.dismiss(); snackbar = null; } } }; prefsManager = new DBStatusManager(getApplicationContext(), updatelistener); prefsManager.registerListener(); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// //SETUP LOCATION locmgr = (LocationManager) getSystemService(LOCATION_SERVICE); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); //We want the nearby bus stops! handler.post(new NearbyStopsRequester()); //If there are no providers available, then, wait for them Log.d("MainActivity", "Created"); } /** * Reload bus stop timetable when it's fulled resumed from background. */ /** * @Override protected void onPostResume() { * super.onPostResume(); * Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop()); * if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) { * setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID); * //new asyncWgetBusStopFromBusStopID(lastSuccessfullySearchedBusStop.ID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); * new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); * } else { * //we have new activity or we don't have a new searched stop. * //Let's search stops nearby * LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); * Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.resultFrame); * <p> * <p> * } * //show the FAB since it remains hidden * floatingActionButton.show(); * <p> * } **/ @Override protected void onPause() { super.onPause(); fh.stopLastRequestIfNeeded(); fh.setBlockAllActivities(true); if (updatelistener != null && prefsManager != null) prefsManager.unregisterListener(); locmgr.removeUpdates(locListener); } @Override protected void onResume() { super.onResume(); fh.setBlockAllActivities(false); if (updatelistener != null && prefsManager != null) { prefsManager.registerListener(); if (prefsManager.isDBUpdating(true)) { createDefaultSnackbar(); } } if (pendingNearbyStopsRequest) handler.post(new NearbyStopsRequester()); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); actionHelpMenuItem = menu.findItem(R.id.action_help); return true; } /** * Callback fired when a MenuItem is selected * * @param item * @return */ @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. switch (item.getItemId()) { case android.R.id.home: // Respond to the action bar's Up/Home button NavUtils.navigateUpFromSameTask(this); return true; case R.id.action_help: showHints(); return true; case R.id.action_favorites: startActivity(new Intent(ActivityMain.this, ActivityFavorites.class)); return true; + case R.id.action_map: + startActivity(new Intent(ActivityMain.this, ActivityMap.class)); + return true; case R.id.action_about: startActivity(new Intent(ActivityMain.this, ActivityAbout.class)); return true; case R.id.action_news: openIceweasel("https://gitpull.it/w/librebusto/#how-to-get-news"); return true; case R.id.action_bugs: openIceweasel("https://gitpull.it/w/librebusto/#how-to-create-a-bug-feature"); return true; case R.id.action_source: openIceweasel("https://gitpull.it/w/librebusto/#how-to-hack-busto"); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html"); return true; case R.id.action_settings: Log.d("MAINBusTO", "Pressed button preferences"); startActivity(new Intent(ActivityMain.this, ActivitySettings.class)); } return super.onOptionsItemSelected(item); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); //OLD ASYNCTASK //new asyncWgetBusStopFromBusStopID(busStopID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); if (busStopID == null || busStopID.length() <= 0) { showMessage(R.string.insert_bus_stop_number_error); toggleSpinner(false); } else { new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(busStopID); Log.d("MainActiv", "Started search for arrivals of stop " + busStopID); } } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); new AsyncDataDownload(AsyncDataDownload.RequestType.STOPS, fh).execute(query); } } /** * PERMISSION STUFF **/ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case PERMISSION_REQUEST_POSITION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { setOption(LOCATION_PERMISSION_GIVEN, true); //if we sent a request for a new NearbyStopsFragment if (pendingNearbyStopsRequest) { pendingNearbyStopsRequest = false; handler.post(new NearbyStopsRequester()); } } else { //permission denied setOption(LOCATION_PERMISSION_GIVEN, false); } //add other cases for permissions } } @Override public void createFragmentForStop(String ID) { //new asyncWgetBusStopFromBusStopID(ID, ArrivalFetchersRecursionHelper,lastSuccessfullySearchedBusStop); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showMessage(R.string.insert_bus_stop_number_error); toggleSpinner(false); } else { new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(ID); Log.d("MainActiv", "Started search for arrivals of stop " + ID); } } /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(this); integrator.initiateScan(); } /** * Receive the Barcode Scanner Intent */ public void onActivityResult(int requestCode, int resultCode, Intent intent) { IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); Uri uri; try { uri = Uri.parse(scanResult != null ? scanResult.getContents() : null); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { Toast.makeText(getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); createFragmentForStop(busStopID); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } 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(); } } } private void createDefaultSnackbar() { if (snackbar == null) { snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); } snackbar.show(); } ///////////////////////////////// POSITION STUFF////////////////////////////////////////////// private void resolveStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { pendingNearbyStopsRequest = false; handler.post(new NearbyStopsRequester()); } } final LocationListener locListener = new LocationListener() { @Override public void onLocationChanged(Location location) { Log.d(DEBUG_TAG, "Location changed"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d(DEBUG_TAG, "Location provider status: " + status); if (status == LocationProvider.AVAILABLE) { resolveStopRequest(provider); } } @Override public void onProviderEnabled(String provider) { resolveStopRequest(provider); } @Override public void onProviderDisabled(String provider) { } }; class NearbyStopsRequester implements Runnable { @SuppressLint("MissingPermission") @Override public void run() { final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); if (!canRunPosition) { pendingNearbyStopsRequest = true; assertLocationPermissions(); return; } else setOption(LOCATION_PERMISSION_GIVEN, true); LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); if (locManager == null) { Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); return; } if (anyLocationProviderMatchesCriteria(locManager, cr, true) && fh.getLastSuccessfullySearchedBusStop() == null) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = framan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = framan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(R.id.resultFrame, fragment, "nearbyStop_correct"); ft.commit(); framan.executePendingTransactions(); pendingNearbyStopsRequest = false; } else if (!anyLocationProviderMatchesCriteria(locManager, cr, true)) { //Wait for the providers Log.d(DEBUG_TAG, "Queuing position request"); pendingNearbyStopsRequest = true; locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, locListener); } } } private boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) { List<String> providers = mng.getProviders(cr, enabled); Log.d(DEBUG_TAG, "Getting enabled location providers: "); for (String s : providers) { Log.d(DEBUG_TAG, "Provider " + s); } return providers.size() > 0; } ///////////////////////////////// OTHER STUFF ////////////////////////////////////////////////// /** * Get the last successfully searched bus stop or NULL * * @return */ @Override public Stop getLastSuccessfullySearchedBusStop() { return fh.getLastSuccessfullySearchedBusStop(); } /** * Get the last successfully searched bus stop ID or NULL * * @return */ @Override public String getLastSuccessfullySearchedBusStopID() { Stop stop = getLastSuccessfullySearchedBusStop(); return stop == null ? null : stop.ID; } /** * Update the star "Add to favorite" icon */ @Override public void updateStarIconFromLastBusStop() { // no favorites no party! addToFavorites = (ImageButton) findViewById(R.id.addToFavorites); if (addToFavorites == null) { Log.d("MainActivity", "Why the fuck the star is not here?!"); return; } // check if there is a last Stop String stopID = getLastSuccessfullySearchedBusStopID(); 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); } } /** * Check if the last Bus Stop is in the favorites * * @return */ public boolean isStopInFavorites(String busStopId) { boolean found = false; // no stop no party if (busStopId != null) { SQLiteDatabase userDB = new UserDB(getApplicationContext()).getReadableDatabase(); found = UserDB.isStopInFavorites(userDB, busStopId); } return found; } /** * Add the last Stop to favorites */ @Override public void toggleLastStopToFavorites() { Stop stop = getLastSuccessfullySearchedBusStop(); if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE) { /** * Callback fired when the Stop is saved in the favorites * @param result */ @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); // update the star icon updateStarIconFromLastBusStop(); } }.execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(); } } @Override public void showFloatingActionButton(boolean yes) { if (yes) floatingActionButton.show(); else floatingActionButton.hide(); } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// @Override public void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } @Override public void showMessage(int messageID) { Toast.makeText(getApplicationContext(), messageID, Toast.LENGTH_SHORT).show(); } 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); } /** * 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); } //TODO: toggle spinner from mainActivity @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); } /** * 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)) { locmgr.removeUpdates(locListener); 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.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment"); return; } // Shows hints } /** * Open an URL in the default browser. * * @param url URL */ public void openIceweasel(String url) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(browserIntent1); } ///////////////////// 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 void changeStarType(String stopID) { if (isStopInFavorites(stopID)) { changeStarFilled(); } else { changeStarOutline(); } } public void changeStarFilled() { addToFavorites.setImageResource(R.drawable.ic_star_filled); } public void changeStarOutline() { addToFavorites.setImageResource(R.drawable.ic_star_outline); } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityMap.java b/src/it/reyboz/bustorino/ActivityMap.java new file mode 100644 index 0000000..8609bdb --- /dev/null +++ b/src/it/reyboz/bustorino/ActivityMap.java @@ -0,0 +1,212 @@ +package it.reyboz.bustorino; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.PreferenceManager; + +import org.osmdroid.api.IMapController; +import org.osmdroid.config.Configuration; +import org.osmdroid.events.DelayedMapListener; +import org.osmdroid.events.MapListener; +import org.osmdroid.events.ScrollEvent; +import org.osmdroid.events.ZoomEvent; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.BoundingBox; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.infowindow.InfoWindow; + +import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.map.CustomInfoWindow; +import it.reyboz.bustorino.middleware.StopsDB; + +public class ActivityMap extends AppCompatActivity { + + private static MapView map = null; + public Context ctx; + + @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //handle permissions first, before map is created. not depicted here + + //load/initialize the osmdroid configuration, this can be done + ctx = getApplicationContext(); + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); + //setting this before the layout is inflated is a good idea + //it 'should' ensure that the map has a writable location for the map cache, even without permissions + //if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath + //see also StorageUtils + //note, the load method also sets the HTTP User Agent to your application's package name, abusing osm's tile servers will get you banned based on this string + + //inflate and create the map + setContentView(R.layout.activity_map); + + map = (MapView) findViewById(R.id.map); + map.setTileSource(TileSourceFactory.MAPNIK); + + // add ability to zoom with 2 fingers + map.setMultiTouchControls(true); + + // take the parameters if it's called from other Activities + Bundle b = getIntent().getExtras(); + GeoPoint marker = null; + String name = null; + String ID = null; + if(b != null) { + double lat = b.getDouble("lat"); + double lon = b.getDouble("lon"); + marker = new GeoPoint(lat, lon); + name = b.getString("name"); + ID = b.getString("ID"); + } + startMap(marker, name, ID); + + // on drag and zoom reload the markers + map.addMapListener(new DelayedMapListener(new MapListener() { + + @Override + public boolean onScroll(ScrollEvent paramScrollEvent) { + loadMarkers(); + return true; + } + + @Override + public boolean onZoom(ZoomEvent event) { + loadMarkers(); + return true; + } + + })); + } + + public void startMap(GeoPoint marker, String name, String ID) { + + // move the map on the marker position or on a default view point: Turin, Piazza Castello + // and set the start zoom + IMapController mapController = map.getController(); + GeoPoint startPoint; + if (marker != null) { + startPoint = marker; + mapController.setZoom(20.0); + } else { + startPoint = new GeoPoint(45.0708, 7.6858); + mapController.setZoom(18.0); + } + // set the minimum zoom level + map.setMinZoomLevel(15.5); + mapController.setCenter(startPoint); + + loadMarkers(); + if (marker != null) { + // make a marker with the info window open for the searched marker + makeMarker(startPoint, name , ID, true); + } + + } + + public void makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) { + + // add a marker + Marker marker = new Marker(map); + + // set custom info window as info window + CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName); + marker.setInfoWindow(popup); + + // make the marker clickable + marker.setOnMarkerClickListener((thisMarker, mapView) -> { + if (thisMarker.isInfoWindowOpen()) { + // on second click + + // create an intent with these extras + Intent intent = new Intent(ActivityMap.this, ActivityMain.class); + Bundle b = new Bundle(); + b.putString("bus-stop-ID", ID); + b.putString("bus-stop-display-name", stopName); + intent.putExtras(b); + intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + + // start ActivityMain with the previous intent + startActivity(intent); + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map); + // show this particular info window + thisMarker.showInfoWindow(); + // move the map to its position + map.getController().animateTo(thisMarker.getPosition()); + } + + return true; + }); + + // set its position + marker.setPosition(geoPoint); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + // display the marker + map.getOverlays().add(marker); + // add to it an icon + marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + // add to it a title + marker.setTitle(stopName); + + // show popup info window of the searched marker + if (isStartMarker) { + marker.showInfoWindow(); + } + + } + + public void loadMarkers() { + + // get rid of the previous markers + map.getOverlays().clear(); + + // get the top, bottom, left and right screen's coordinate + BoundingBox bb = map.getBoundingBox(); + double latFrom = bb.getLatSouth(); + double latTo = bb.getLatNorth(); + double lngFrom = bb.getLonWest(); + double lngTo = bb.getLonEast(); + + // get the stops located in those coordinates + StopsDB stopsDB = new StopsDB(ctx); + stopsDB.openIfNeeded(); + Stop[] stops = stopsDB.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo); + stopsDB.closeIfNeeded(); + + // add new markers of those stops + for (Stop stop : stops) { + GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); + makeMarker(marker, stop.getStopDefaultName(), stop.ID, false); + } + + } + + public void onResume(){ + super.onResume(); + //this will refresh the osmdroid configuration on resuming. + //if you make changes to the configuration, use + //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + //Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this)); + map.onResume(); //needed for compass, my location overlays, v6.0.0 and up + } + + public void onPause(){ + super.onPause(); + //this will refresh the osmdroid configuration on resuming. + //if you make changes to the configuration, use + //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + //Configuration.getInstance().save(this, prefs); + map.onPause(); //needed for compass, my location overlays, v6.0.0 and up + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/map/CustomInfoWindow.java b/src/it/reyboz/bustorino/map/CustomInfoWindow.java new file mode 100644 index 0000000..c1fcab5 --- /dev/null +++ b/src/it/reyboz/bustorino/map/CustomInfoWindow.java @@ -0,0 +1,42 @@ +package it.reyboz.bustorino.map; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; + +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.infowindow.BasicInfoWindow; + +import it.reyboz.bustorino.ActivityMain; +import it.reyboz.bustorino.R; + +public class CustomInfoWindow extends BasicInfoWindow { + + @SuppressLint("ClickableViewAccessibility") + public CustomInfoWindow(MapView mapView, String ID, String stopName) { + // get the personalized layout + super(R.layout.map_popup, mapView); + + // make clickable + mView.setOnTouchListener((View v, MotionEvent e) -> { + if (e.getAction() == MotionEvent.ACTION_UP) { + // on click + + // create an intent with these extras + Intent intent = new Intent(mapView.getContext(), ActivityMain.class); + Bundle b = new Bundle(); + b.putString("bus-stop-ID", ID); + b.putString("bus-stop-display-name", stopName); + intent.putExtras(b); + intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + + // start ActivityMain with the previous intent + mapView.getContext().startActivity(intent); + } + return true; + }); + } + +}