diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8c81b54..c099060 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,133 +1,135 @@ - + + android:value="it.reyboz.bustorino.ActivityPrincipal"/> diff --git a/build.gradle b/build.gradle index 6344f9d..3833e4b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,117 +1,117 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.2' + classpath 'com.android.tools.build:gradle:4.1.3' } ext { //libraries versions fragment_version = "1.3.3" activity_version = "1.1.0" appcompat_version = "1.2.0" 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" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() } } apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion '29.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 14 targetSdkVersion 29 versionCode 32 versionName "1.14.1" 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 { //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.3.0' + implementation "com.google.android.material:material:1.3.0" implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.0' 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" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c6088f1..7dc96cb 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 01 23:03:06 CEST 2020 +#Sat Apr 24 16:03:07 CEST 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/res/layout/nav_header.xml b/res/layout/nav_header.xml index c03c443..0b7e16d 100644 --- a/res/layout/nav_header.xml +++ b/res/layout/nav_header.xml @@ -1,30 +1,33 @@ - + android:theme="@style/ThemeOverlay.AppCompat.Dark"> + + android:textColor="#FFFFFF" + android:textSize="20sp" /> \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index bdea437..ca32eab 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,149 +1,152 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:

Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!

Come Funziona?

Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".

Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori!

Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.

Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Numero di fermate Impostazioni Impostazioni Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database - + Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS Raggio di ricerca Funzionalità sperimentali arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Canale unico delle notifiche Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio - L\'app è andata in crash. Invia il report agli sviluppatori toccando OK. - \n Il report potrebbe contenere informazioni sulla tua configurazione del telefono, o sullo stato al momento del crash. - \n Tutte le informazioni sensibili nel report verranno utilizzate solo per scopi diagnostici dagli sviluppatori, e non verranno mai pubblicate e/o divulgate. - L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima del crash: \n + 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. + + L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè + Mappa + Ricerca fermate
diff --git a/res/values/colors.xml b/res/values/colors.xml index b37a5dd..2a1ba0e 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -1,18 +1,19 @@ #ff9800 #F57C00 #2196F3 #2060dd #009688 #4DB6AC #80cbc4 #F5F5F5 #757575 #DE0908 #2060DD + #FFFFFF \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 482bb16..163e0c5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,165 +1,168 @@ BusTO + Libre BusTO BusTO Debug You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Lines: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.

Why use this app?

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

How does it work?

This app will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".

Who worked on BusTO:
- Fabio Mazza current senior rockstar developer.
- Andrea Ugo current junior rockstar developer.
- Silviu Chiriac designer of the 2021 logo.
- Ludovico Pavesi previous senior rockstar developer asd.
- Valerio Bozzolan maintainer and infrastructure sponsor.
- Marco Gagino contributor and icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

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

Notes

This app has been developed hoping to be useful to everyone but without ANY warranty.

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

Get involved! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections Finding the position… No stops nearby Number of stops Preferences Settings Settings Experimental features Search radius Recent stops General settings Database management Launch manual database update + Allow access to position to show it on the map Please enable GPS Database update in progress… is arriving at at the stop %1$s - %2$s Show arrivals Show stops Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Changing arrival times source… Long press to change the source of arrivals Default Default channel for notifications Asked for %1$s permission too many times Cannot use the map with the storage permission! storage - The application has crashed. If you want, you can send the send the report via email to - the developers by pressing \"OK\". - \n Note that sensitive information may be contained in the report, and if so, it will be only used for - diagnostic purposes by the developers, and never published. + The application has crashed because you encountered a bug. + \nIf you want, you can help the developers by sending the crash report via email. + \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee + Map + Search by stop
diff --git a/src/debug/AndroidManifest.xml b/src/debug/AndroidManifest.xml index 566ff12..6b7101f 100644 --- a/src/debug/AndroidManifest.xml +++ b/src/debug/AndroidManifest.xml @@ -1,29 +1,29 @@ diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 5d26f66..5dc6742 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,838 +1,837 @@ /* 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 . */ package it.reyboz.bustorino; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import androidx.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.snackbar.Snackbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.core.app.NavUtils; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; 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 com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; import it.reyboz.bustorino.util.Permissions; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; public class ActivityMain extends GeneralActivity implements FragmentListenerMain { /* * Layout elements */ private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem, experimentsMenuItem; 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 static final String DEBUG_TAG = "BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference 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 ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; private StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; /* * Position */ //Fine location criteria private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// /* * @see swipeRefreshLayout */ private final Handler theHandler = new Handler(); private final Runnable refreshStop = new Runnable() { public void run() { if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment == null){ new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray(), getApplicationContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute(); } }; //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); final SharedPreferences theShPr = getMainSharedPreferences(); /* * UI */ setContentView(R.layout.activity_main); Toolbar defToolbar = findViewById(R.id.that_toolbar); setSupportActionBar(defToolbar); busStopSearchByIDEditText = findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = findViewById(R.id.busStopSearchByNameEditText); progressBar = findViewById(R.id.progressBar); howDoesItWorkTextView = findViewById(R.id.howDoesItWorkTextView); hideHintButton = findViewById(R.id.hideHintButton); swipeRefreshLayout = findViewById(R.id.listRefreshLayout); floatingActionButton = findViewById(R.id.floatingActionButton); framan.addOnBackStackChangedListener(() -> Log.d("MainActivity, BusTO", "BACK STACK CHANGED")); 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; }); // Called when the layout is pulled down swipeRefreshLayout .setOnRefreshListener(() -> theHandler.post(refreshStop)); /** * @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, framan, getApplicationContext(),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 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); */ requestArrivalsForStopID(busStopID); } //Try (hopefully) database update PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); final WorkManager workManager = WorkManager.getInstance(this); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); if (version >= 0) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); /* Set database update */ workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); //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! theHandler.post(new NearbyStopsRequester(this)); //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 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); *

*

* } * //show the FAB since it remains hidden * floatingActionButton.show(); *

* } **/ @Override protected void onPause() { super.onPause(); fh.stopLastRequestIfNeeded(); fh.setBlockAllActivities(true); locmgr.removeUpdates(locListener); } @Override protected void onResume() { super.onResume(); fh.setBlockAllActivities(false); //TODO: check if current LiveData-bound observer works if (pendingNearbyStopsRequest) theHandler.post(new NearbyStopsRequester(this)); ActionBar bar = getSupportActionBar(); if(bar!=null) bar.show(); else Log.w(DEBUG_TAG, "ACTION BAR IS NULL"); //check if we can display the experiments or not SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); //Log.w(DEBUG_TAG, "Preference experimental is "+exper_On); //MenuItem experimentsItem = if (experimentsMenuItem != null) experimentsMenuItem.setVisible(exper_On); } @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); experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); 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. Resources res = getResources(); 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: //ensure storage permission is granted final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); switch (result) { case PERMISSION_OK: startActivity(new Intent(ActivityMain.this, ActivityMap.class)); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> startActivity(new Intent(ActivityMain.this, ActivityMap.class))); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = res.getString(R.string.storage_permission); String text = res.getString(R.string.too_many_permission_asks, storage_perm); Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } return true; case R.id.action_about: startActivity(new Intent(ActivityMain.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(res.getString(R.string.hack_url)); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/"); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html"); return true; case R.id.action_donate: openIceweasel("https://www.liberapay.com/Libre_BusTO/"); return true; case R.id.action_settings: Log.d("MAINBusTO", "Pressed button preferences"); startActivity(new Intent(ActivityMain.this, ActivitySettings.class)); return true; case R.id.action_experiments: startActivity(new Intent(this, ActivityPrincipal.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(); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); new AsyncDataDownload(fh, stopsFinderByNames, getApplicationContext()).execute(query); } } /** * PERMISSION STUFF **/ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, 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; theHandler.post(new NearbyStopsRequester(this)); } } else { //permission denied setOption(LOCATION_PERMISSION_GIVEN, false); } //add other cases for permissions break; case STORAGE_PERMISSION_REQ: final String storageKey = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storageKey)) { Runnable toRun = permissionDoneRunnables.get(storageKey); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storageKey); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); /*final int canGetPermission = askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, STORAGE_PERMISSION_REQ); switch (canGetPermission) { case PERMISSION_ASKING: break; case PERMISSION_NEG_CANNOT_ASK: permissionDoneRunnables.remove(storageKey); showToastMessage(R.string.closing_act_crash_msg, false); }*/ } } } @Override public void requestArrivalsForStopID(String ID) { 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(fh,fragment.getCurrentFetchersAsArray(), this).execute(ID); } else{ new AsyncDataDownload(fh, arrivalsFetchers, this).execute(ID); } } else { new AsyncDataDownload(fh, arrivalsFetchers, this).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) { super.onActivityResult(requestCode, resultCode, 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); requestArrivalsForStopID(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; theHandler.post(new NearbyStopsRequester(this)); } } 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) { } }; /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Activity runningAct; public NearbyStopsRequester(Activity runningAct) { this.runningAct = runningAct; } @Override public void run() { - final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); - final boolean noPermission = ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; + //final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); + final boolean noPermission = ContextCompat.checkSelfPermission(getApplicationContext(), + Manifest.permission.ACCESS_FINE_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 (!canRunPosition) { + pendingNearbyStopsRequest = true; Permissions.assertLocationPermissions(getApplicationContext(),runningAct); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; - } else { - Toast.makeText(getApplicationContext(), "Asked for permission position too many times", Toast.LENGTH_LONG).show(); - } + } 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 && !framan.isDestroyed()) { //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 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 ////////////////////////////////////////////////// @Override public void showFloatingActionButton(boolean yes) { if (yes) floatingActionButton.show(); else floatingActionButton.hide(); } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// 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); } 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 } private void openIceweasel(String url){ utils.openIceweasel(url, this); } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java index f900b2e..7f8a0db 100644 --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,480 +1,557 @@ package it.reyboz.bustorino; +import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; +import java.util.Arrays; import java.util.concurrent.TimeUnit; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.fragments.FavoritesFragment; import it.reyboz.bustorino.fragments.FragmentKind; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.fragments.MainScreenFragment; import it.reyboz.bustorino.fragments.MapFragment; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener()); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); + mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { + + } + + @Override + public void onDrawerOpened(@NonNull View drawerView) { + hideKeyboard(); + } + + @Override + public void onDrawerClosed(@NonNull View drawerView) { + + } + + @Override + public void onDrawerStateChanged(int newState) { + } + }); + + mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); - /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = 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"); /** * 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 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); requestArrivalsForStopID(busStopID); } //Try (hopefully) database update - - PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); final WorkManager workManager = WorkManager.getInstance(this); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); if (version >= 0) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); /* Set database update */ workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment showMainFragment(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } + + /** + * Setup drawer actions + * @param navigationView the navigation view on which to set the callbacks + */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); FavoritesFragment fragment = FavoritesFragment.newInstance(); ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES); ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); - createAndShowMapFragment(null); + final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; + int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); + switch (result) { + case PERMISSION_OK: + createAndShowMapFragment(null); + break; + case PERMISSION_ASKING: + permissionDoneRunnables.put(permission, + () -> createAndShowMapFragment(null)); + break; + case PERMISSION_NEG_CANNOT_ASK: + String storage_perm = getString(R.string.storage_permission); + String text = getString(R.string.too_many_permission_asks, storage_perm); + Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); + } return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem.toString()); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.extra_menu_items, menu); return super.onCreateOptionsMenu(menu); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode==STORAGE_PERMISSION_REQ){ + final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); + + if (permissionDoneRunnables.containsKey(storagePerm)) { + Runnable toRun = permissionDoneRunnables.get(storagePerm); + if (toRun != null) + toRun.run(); + permissionDoneRunnables.remove(storagePerm); + } + } else { + //permission denied + showToastMessage(R.string.permission_storage_maps_msg, false); + } + } + } + @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { boolean foundFragment = false; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main shownFrag.getChildFragmentManager().popBackStackImmediate(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); } else super.onBackPressed(); } private void createDefaultSnackbar() { if (snackbar == null) { snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); } snackbar.show(); } private MainScreenFragment createAndShowMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); MainScreenFragment fragment = MainScreenFragment.newInstance(); FragmentTransaction transaction = fraMan.beginTransaction(); transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG); transaction.commit(); return fragment; } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment){ fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment) .setReorderingAllowed(true) .addToBackStack(null) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private MainScreenFragment showMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); MainScreenFragment mainScreenFragment = null; if (fragment==null | !(fragment instanceof MainScreenFragment)){ mainScreenFragment = createAndShowMainFragment(); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.readyGUIfor(fragmentType); } + int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); + titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); + titleResId = R.string.nav_favorites_text; break; case ARRIVALS: - case NEARBY_STOPS: + titleResId = R.string.nav_arrivals_text; + mNavView.setCheckedItem(R.id.nav_arrivals); + break; case STOPS: + titleResId = R.string.stop_search_view_title; + mNavView.setCheckedItem(R.id.nav_arrivals); + break; case MAIN_SCREEN_FRAGMENT: + case NEARBY_STOPS: case NEARBY_ARRIVALS: + titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; + default: + titleResId = 0; } + if(getSupportActionBar()!=null && titleResId!=0) + getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment); } else { // we have no fragment probableFragment = createAndShowMainFragment(); } } probableFragment.requestArrivalsForStopID(ID); mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_about: startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(getString(R.string.hack_url), getApplicationContext()); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", getApplicationContext()); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", getApplicationContext()); return true; default: } return false; } } } diff --git a/src/it/reyboz/bustorino/BustoApp.java b/src/it/reyboz/bustorino/BustoApp.java index f656f6a..9cc4e72 100644 --- a/src/it/reyboz/bustorino/BustoApp.java +++ b/src/it/reyboz/bustorino/BustoApp.java @@ -1,36 +1,42 @@ package it.reyboz.bustorino; import android.app.Application; import android.content.Context; import org.acra.ACRA; import org.acra.BuildConfig; -import org.acra.annotation.AcraCore; -import org.acra.annotation.AcraDialog; -import org.acra.annotation.AcraMailSender; +import org.acra.ReportField; import org.acra.config.CoreConfigurationBuilder; import org.acra.config.DialogConfigurationBuilder; import org.acra.config.MailSenderConfigurationBuilder; import org.acra.data.StringFormat; +import static org.acra.ReportField.*; + public class BustoApp extends Application { + private static final ReportField[] REPORT_FIELDS = {REPORT_ID, APP_VERSION_CODE, APP_VERSION_NAME, + PACKAGE_NAME, PHONE_MODEL, BRAND, PRODUCT, ANDROID_VERSION, BUILD_CONFIG, CUSTOM_DATA, + IS_SILENT, STACK_TRACE, INITIAL_CONFIGURATION, CRASH_CONFIGURATION, DISPLAY, USER_COMMENT, + USER_APP_START_DATE, USER_CRASH_DATE, LOGCAT, SHARED_PREFERENCES}; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); CoreConfigurationBuilder builder = new CoreConfigurationBuilder(this); builder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.JSON) .setDeleteUnapprovedReportsOnApplicationStart(true); builder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setMailTo("gtt@succhia.cz") .setReportFileName(it.reyboz.bustorino.BuildConfig.VERSION_NAME +"_report.json") .setResBody(R.string.acra_email_message) .setEnabled(true); builder.getPluginConfigurationBuilder(DialogConfigurationBuilder.class).setResText(R.string.message_crash) .setResTheme(R.style.AppTheme) .setEnabled(true); + builder.setReportContent(REPORT_FIELDS); if (!it.reyboz.bustorino.BuildConfig.DEBUG) ACRA.init(this, builder); + } } diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index 4a2a28f..e1d2e08 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,99 +1,106 @@ 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; 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 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; } /** * 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)); context.startActivity(browserIntent1); } } diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java index 2abc239..853abf9 100644 --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,505 +1,506 @@ /* 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 . */ 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.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 { 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 fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); 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(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); } } @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 getCurrentFetchers(){ ArrayList v = new ArrayList(); 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 UNDETERMINED: //Don't show the view timesSourceTextView.setVisibility(View.GONE); return; default: throw new IllegalStateException("Unexpected value: " + source); } int count = 0; 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 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 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 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/CommonFragmentListener.java b/src/it/reyboz/bustorino/fragments/CommonFragmentListener.java index fa7cc43..b849b0b 100644 --- a/src/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/src/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -1,29 +1,31 @@ package it.reyboz.bustorino.fragments; +import android.view.View; + public interface CommonFragmentListener { /** * Tell the activity that we need to disable/enable its floatingActionButton * @param yes or no */ void showFloatingActionButton(boolean yes); /** * Sends the message to the activity to adapt the GUI * to the fragment that has been attached * @param fragmentType the type of fragment attached */ void readyGUIfor(FragmentKind fragmentType); /** * Houston, we need another fragment! * * @param ID the Stop ID */ void requestArrivalsForStopID(String ID); /** * Method to call when we want to hide the keyboard */ void hideKeyboard(); } diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java index 247f697..6435af2 100644 --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,281 +1,284 @@ package it.reyboz.bustorino.fragments; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; +import java.util.ArrayList; import java.util.List; import it.reyboz.bustorino.ActivityFavorites; import it.reyboz.bustorino.ActivityMain; import it.reyboz.bustorino.ActivityMap; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.StopAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.FavoritesViewModel; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class FavoritesFragment extends BaseFragment { private ListView favoriteListView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; @Nullable private CommonFragmentListener mListener; public static final String FRAGMENT_TAG = "BusTOFavFragment"; public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteListView = root.findViewById(R.id.favoriteListView); favoriteListView.setOnItemClickListener((parent, view, position, 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); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); registerForContextMenu(favoriteListView); FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); + + showStops(new ArrayList<>()); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context.toString() + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (v.getId() == R.id.favoriteListView) { // if we aren't attached to activity, return null if (getActivity()==null) return; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public void onResume() { super.onResume(); if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); Stop busStop = (Stop) favoriteListView.getItemAtPosition(info.position); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, result -> { }).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(getContext(),R.string.cannot_show_on_map_no_position,Toast.LENGTH_SHORT).show(); return true; } // start ActivityMap with these extras in intent Intent intent = new Intent(getContext(), ActivityMap.class); Bundle b = new Bundle(); double lat, lon; if (busStop.getLatitude()!=null) lat = busStop.getLatitude(); else lat = 200; if (busStop.getLongitude()!=null) lon = busStop.getLongitude(); else lon = 200; b.putDouble("lat", lat); b.putDouble("lon",lon); b.putString("name", busStop.getStopDefaultName()); b.putString("ID", busStop.ID); intent.putExtras(b); startActivity(intent); TODO: start map on button press */ return true; default: return super.onContextItemSelected(item); } } void showStops(List 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); } else { favoriteListView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* 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(getContext(), busStops)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.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 = busStopNameText.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); } } else { // changed to something // something different? if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); } } launchUpdate(busStop); } }); 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); launchUpdate(busStop); } }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, new AsyncStopFavoriteAction.ResultListener() { @Override public void doStuffWithResult(Boolean result) { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); } }).execute(busStop); } } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index 5c82c99..02c4258 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,638 +1,699 @@ 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.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; 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.ArrivalsFetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.FiveTScraperFetcher; import it.reyboz.bustorino.backend.FiveTStopsFetcher; import it.reyboz.bustorino.backend.GTTJSONFetcher; import it.reyboz.bustorino.backend.GTTStopsFetcher; +import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.StopsFinderByName; +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 android.content.Context.LOCATION_SERVICE; +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()}; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { 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) { - /// LOCATION STUFF /// - boolean pendingNearbyStopsRequest = false; - LocationManager locmgr; + } + + @Override + public void onLocationStatusChanged(int status) { + + if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown()){ + //request Stops + pendingNearbyStopsRequest = false; + + 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()){ + pendingNearbyStopsRequest = false; + mainHandler.post(new NearbyStopsRequester(getContext(), cr)); + } + } + + @Override + public void onLocationDisabled() { + + } + }; + private final ActivityResultLauncher requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { + @Override + public void onActivityResult(Map 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); + } + } + } + }); - private final Criteria cr = new Criteria(); //// 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); - locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE); + 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); } 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); } } */ } @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.toString() + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! - mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener)); + mainHandler.post(new NearbyStopsRequester(context, cr)); else{ ///TODO: if there is a stop displayed, we need to hold the update } - //If there are no providers available, then, wait for them setupOnAttached = false; - } else { } + } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); - if (con != null) - locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE); + 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); } @Override public void onPause() { //mainHandler = null; - locmgr = null; + locationManager.removeLocationRequestFor(requester); super.onPause(); } /* 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(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); 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; + 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)) { - locmgr.removeUpdates(locListener); + 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 } /** * 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 ////////// - 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) { - } - }; - - private void resolveStopRequest(String provider) { + /* + private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { - pendingNearbyStopsRequest = false; - mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener)); + } } + */ + /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; - LocationListener listener; - public NearbyStopsRequester(Context appContext, Criteria criteria, LocationListener listener) { + public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; - this.listener = listener; } @Override public void run() { - final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); + 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 (!canRunPosition) { + if (!isOldVersion) { pendingNearbyStopsRequest = true; - Permissions.assertLocationPermissions(appContext,getActivity()); + //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); - LocationManager locManager = (LocationManager) appContext.getSystemService(LOCATION_SERVICE); - if (locManager == null) { - Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); - return; - } - if (Permissions.anyLocationProviderMatchesCriteria(locManager, cr, 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"); - 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, "nearbyStop_correct"); - ft.commit(); - //fragMan.executePendingTransactions(); + showNearbyStopsFragment(); pendingNearbyStopsRequest = false; - } else if (!Permissions.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, listener); + } 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/MapFragment.java b/src/it/reyboz/bustorino/fragments/MapFragment.java index f7630e0..ee6c047 100644 --- a/src/it/reyboz/bustorino/fragments/MapFragment.java +++ b/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,532 +1,582 @@ package it.reyboz.bustorino.fragments; import android.Manifest; +import android.annotation.SuppressLint; import android.content.Context; -import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; +import android.widget.Toast; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; import androidx.core.content.res.ResourcesCompat; import androidx.preference.PreferenceManager; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.map.CustomInfoWindow; import it.reyboz.bustorino.map.LocationOverlay; import it.reyboz.bustorino.middleware.GeneralActivity; - -import static it.reyboz.bustorino.util.Permissions.PERMISSION_REQUEST_POSITION; +import it.reyboz.bustorino.util.Permissions; public class MapFragment extends BaseFragment { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; private boolean followingLocation = false; protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; + private final ActivityResultLauncher positionRequestLauncher = + registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { + @Override + @SuppressLint("MissingPermission") + public void onActivityResult(Map result) { + if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ + + map.getOverlays().remove(mLocationOverlay); + startLocationOverlay(true); + if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) + return; + LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); + Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (userLocation != null) { + map.getController().setZoom(POSITION_FOUND_ZOOM); + GeoPoint startPoint = new GeoPoint(userLocation); + setLocationFollowing(true); + map.getController().setCenter(startPoint); + } + } + else Log.w(DEBUG_TAG,"No location permission"); + } + }); + public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(double stopLatit, double stopLong, String stopName, String stopID){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stopLatit); args.putDouble(BUNDLE_LONGIT, stopLong); args.putString(BUNDLE_NAME, stopName); args.putString(BUNDLE_ID, stopID); fragment.setArguments(args); return fragment; } public static MapFragment getInstance(Stop stop){ return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = root.findViewById(R.id.ic_center_map); btFollowMe = root.findViewById(R.id.ic_follow_me); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); - final GeoPoint myPosition = mLocationOverlay.getMyLocation(); - map.getController().animateTo(myPosition); + if(Permissions.locationPermissionGranted(getContext())) { + final GeoPoint myPosition = mLocationOverlay.getMyLocation(); + map.getController().animateTo(myPosition); + } else + Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); - switchLocationFollowing(!followingLocation); + if(Permissions.locationPermissionGranted(getContext())) + setLocationFollowing(!followingLocation); + else + Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); saveMapState(); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); } @Override public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ - public void switchLocationFollowing(Boolean value){ + public void setLocationFollowing(Boolean value){ followingLocation = value; + if(mLocationOverlay==null || getContext() == null) + //nothing else to do + return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } + /** + * Start the location overlay. Enable only when + * a) we know we have the permission + * b) the location map is set + */ + private void startLocationOverlay(boolean enableLocation){ + if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); + // Location Overlay + // from OpenBikeSharing (THANK GOD) + Log.d(DEBUG_TAG, "Starting position overlay"); + GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); + imlp.setLocationUpdateMinDistance(5); + imlp.setLocationUpdateMinTime(2000); + this.mLocationOverlay = new LocationOverlay(imlp,map, locationCallbacks); + if (enableLocation) mLocationOverlay.enableMyLocation(); + mLocationOverlay.setOptionsMenuEnabled(true); + + map.getOverlays().add(this.mLocationOverlay); + } + public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; - boolean havePositionPermission = true; - - if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - activity.askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, PERMISSION_REQUEST_POSITION); - havePositionPermission = false; - } - // Location Overlay - // from OpenBikeSharing (THANK GOD) - GpsMyLocationProvider imlp = new GpsMyLocationProvider(activity.getBaseContext()); - imlp.setLocationUpdateMinDistance(5); - imlp.setLocationUpdateMinTime(2000); - this.mLocationOverlay = new LocationOverlay(imlp,map, locationCallbacks); - mLocationOverlay.enableMyLocation(); - mLocationOverlay.setOptionsMenuEnabled(true); - + // set the center point if (marker != null) { startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); - switchLocationFollowing(false); + setLocationFollowing(false); } else if (savedInstanceState != null) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); - switchLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); + setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); - if (locationManager != null) { + //check for permission + if (locationManager != null && Permissions.locationPermissionGranted(activity)) { + @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; - switchLocationFollowing(true); + setLocationFollowing(true); + } + } else if(!Permissions.locationPermissionGranted(activity)){ + if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); + } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); - mapController.setZoom(16.0); - switchLocationFollowing(false); + mapController.setZoom(17.0); + setLocationFollowing(false); } } + startLocationOverlay(Permissions.locationPermissionGranted(activity)); // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } - - - map.getOverlays().add(this.mLocationOverlay); - //add stops overlay map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker makeMarker(startPoint, name , ID, true); } } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); new AsyncStopFetcher(this).execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker 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, responder); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click //TODO: show the arrivals for the stop Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(ID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); } return marker; } /** * Simple asyncTask class to load the stops in the background * Holds a weak reference to the fragment to do callbacks */ static class AsyncStopFetcher extends AsyncTask>{ final WeakReference fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List doInBackground(BoundingBoxLimit... limits) { if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return null; } final BoundingBoxLimit limit = limits[0]; //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); NextGenDB dbHelper = new NextGenDB(fragmentWeakReference.get().getContext()); Stop[] stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, limit.longFrom, limit.latitTo); dbHelper.close(); return Arrays.asList(stops); } @Override protected void onPostExecute(List stops) { super.onPostExecute(stops); //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); if(fragmentWeakReference.get()==null) { Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return; } Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); fragmentWeakReference.get().showStopsMarkers(stops); } private static class BoundingBoxLimit{ final double longFrom, longTo, latitFrom, latitTo; public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { this.longFrom = longFrom; this.longTo = longTo; this.latitFrom = latitFrom; this.latitTo = latitTo; } } } } diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 0d68be7..bcff049 100644 --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,654 +1,670 @@ /* 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 . */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; private final String[] PROJECTION = {StopsTable.COL_ID,StopsTable.COL_LAT,StopsTable.COL_LONG, StopsTable.COL_NAME,StopsTable.COL_TYPE,StopsTable.COL_LINES_STOPPING}; private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; private int fragment_type; + public final static String FRAGMENT_TAG="NearbyStopsFrag"; + //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; boolean canStartDBQuery = true; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; private AppLocationManager locManager; //These are useful for the case of nearby arrivals private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ public static NearbyStopsFragment newInstance(int fragmentType) { if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); args.putInt(FRAGMENT_TYPE_KEY,fragmentType); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { setFragmentType(getArguments().getInt(FRAGMENT_TYPE_KEY)); } locManager = AppLocationManager.getInstance(getContext()); fragmentLocationListener = new FragmentLocationListener(this); globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences),Context.MODE_PRIVATE); globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment + if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); - gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)); + gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.loadingBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); preferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d(DEBUG_TAG,"Key "+key+" was changed"); if(key.equals(getString(R.string.databaseUpdatingPref))){ if(!sharedPreferences.getBoolean(getString(R.string.databaseUpdatingPref),true)){ canStartDBQuery = true; Log.d(DEBUG_TAG,"The database has finished updating, can start update now"); } } } }; scrollListener = new CommonScrollListener(mListener,false); switchButton.setOnClickListener(v -> { switchFragmentType(); }); Log.d(DEBUG_TAG, "onCreateView"); return root; } protected ArrayList createStopListFromCursor(Cursor data){ ArrayList stopList = new ArrayList<>(); final int col_id = data.getColumnIndex(StopsTable.COL_ID); final int latInd = data.getColumnIndex(StopsTable.COL_LAT); final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); data.moveToFirst(); for(int i=0; i onCreateLoader(int id, Bundle args) { //BUILD URI lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) .appendPath(String.valueOf(distance)); //distance CursorLoader cl = new CursorLoader(getContext(),builder.build(),PROJECTION,null,null,null); cl.setUpdateThrottle(2000); return cl; } @Override public void onLoadFinished(@NonNull Loader loader, Cursor data) { if (0 > MAX_DISTANCE) throw new AssertionError(); //Cursor might be null if(data==null){ Log.e(DEBUG_TAG,"Null cursor, something really wrong happened"); return; } if(!isDBUpdating() && (data.getCount() stopList = createStopListFromCursor(data); if(data.getCount()>0) { //quick trial to hopefully always get the stops in the correct order Collections.sort(stopList,new StopSorterByDistance(lastReceivedLocation)); switch (fragment_type){ case TYPE_STOPS: showStopsInRecycler(stopList); break; case TYPE_ARRIVALS: arrivalsManager = new ArrivalsManager(stopList); flatProgressBar.setVisibility(View.VISIBLE); flatProgressBar.setProgress(0); flatProgressBar.setIndeterminate(false); //for the moment, be satisfied with only one location //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); break; default: } } else { setNoStopsLayout(); } } @Override public void onLoaderReset(Loader loader) { } /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ if(fragment_type==TYPE_ARRIVALS){ setFragmentType(TYPE_STOPS); switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); } else if (fragment_type==TYPE_STOPS){ setFragmentType(TYPE_ARRIVALS); titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; locManager.removeLocationRequestFor(fragmentLocationListener); locManager.addLocationRequestFor(fragmentLocationListener); } //useful methods protected boolean isDBUpdating(){ return globalSharedPref.getBoolean(getString(R.string.databaseUpdatingPref),false); } /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ if(firstLocForStops) { dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); dataAdapter.setUserPosition(lastReceivedLocation); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); + + if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } if(firstLocForArrivals){ arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); + if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); + } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); } class ArrivalsManager implements FiveTAPIVolleyRequest.ResponseListener, Response.ErrorListener{ final HashMap mStops; final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; private final QueryType[] types = {QueryType.ARRIVALS,QueryType.DETAILS}; final NetworkVolleyManager volleyManager; private final int MAX_ARRIVAL_STOPS =35; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ mStops = new HashMap<>(); volleyManager = NetworkVolleyManager.getInstance(getContext()); for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ mStops.put(s.ID,new Palina(s)); for(QueryType t: types) { final FiveTAPIVolleyRequest req = FiveTAPIVolleyRequest.getNewRequest(t, s.ID, this, this); if (req != null) { req.setTag(REQUEST_TAG); volleyManager.addToRequestQueue(req); activeRequestCount++; } } } flatProgressBar.setMax(activeRequestCount); } @Override public void onErrorResponse(VolleyError error) { if(error instanceof ParseError){ //TODO Log.w(DEBUG_TAG,"Parsing error for stop request"); } else if (error instanceof NetworkError){ String s; if(error.networkResponse!=null) s = new String(error.networkResponse.data); else s=""; Log.w(DEBUG_TAG,"Network error: "+s); }else { Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage()); } if(error.networkResponse!=null){ Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode); } //counters activeRequestCount--; reqErrorCount++; flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); } @Override public void onResponse(Palina result, QueryType type) { //counter for requests activeRequestCount--; reqSuccessCount++; final Palina palinaInMap = mStops.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); //necessary to split the Arrivals and Details cases switch (type){ case ARRIVALS: palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); final List possibleRoutes = routesToAdd.get(result.ID); if(possibleRoutes!=null) { palinaInMap.addInfoFromRoutes(possibleRoutes); routesToAdd.remove(result.ID); } break; case DETAILS: if(palinaInMap.queryAllRoutes().size()>0){ //merge the branches palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); } else { routesToAdd.put(result.ID,result.queryAllRoutes()); } break; default: throw new IllegalArgumentException("Wrong QueryType in onResponse"); } final ArrayList outList = new ArrayList<>(); for(Palina p: mStops.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } showArrivalsInRecycler(outList); flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); if(activeRequestCount==0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } } void cancelAllRequests(){ volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); flatProgressBar.setVisibility(View.GONE); } } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements AppLocationManager.LocationRequester{ LoaderManager.LoaderCallbacks callbacks; private int oldLocStatus = -2; private LocationCriteria cr; private long lastUpdateTime = -1; public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { this.callbacks = callbacks; } @Override public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); if(accuracy<60 && canStartDBQuery) { distance = 20; final Bundle msgBundle = new Bundle(); msgBundle.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); Log.d("BusTO:NearPositListen","can start loader "+ canStartDBQuery); } @Override public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override public LocationCriteria getLocationCriteria() { - return new LocationCriteria(60,TIME_INTERVAL_REQUESTS); + return new LocationCriteria(120,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } + + @Override + public void onLocationProviderAvailable() { + + } + + @Override + public void onLocationDisabled() { + + } } /** * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) * */ class AutoFitGridLayoutManager extends GridLayoutManager { private int columnWidth; private boolean columnWidthChanged = true; public AutoFitGridLayoutManager(Context context, int columnWidth) { super(context, 1); setColumnWidth(columnWidth); } public void setColumnWidth(int newColumnWidth) { if (newColumnWidth > 0 && newColumnWidth != columnWidth) { columnWidth = newColumnWidth; columnWidthChanged = true; } } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (columnWidthChanged && columnWidth > 0) { int totalSpace; if (getOrientation() == VERTICAL) { totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); } else { totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); } int spanCount = Math.max(1, totalSpace / columnWidth); setSpanCount(spanCount); columnWidthChanged = false; } super.onLayoutChildren(recycler, state); } } } diff --git a/src/it/reyboz/bustorino/map/LocationOverlay.java b/src/it/reyboz/bustorino/map/LocationOverlay.java index 483a87f..5f991c9 100644 --- a/src/it/reyboz/bustorino/map/LocationOverlay.java +++ b/src/it/reyboz/bustorino/map/LocationOverlay.java @@ -1,62 +1,63 @@ /* BusTO (middleware) Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.map; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.mylocation.IMyLocationProvider; import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; public class LocationOverlay extends MyLocationNewOverlay { final OverlayCallbacks callbacks; public LocationOverlay(MapView mapView, OverlayCallbacks callbacks) { super(mapView); this.callbacks = callbacks; } public LocationOverlay(IMyLocationProvider myLocationProvider, MapView mapView, OverlayCallbacks callbacks) { super(myLocationProvider, mapView); this.callbacks = callbacks; } @Override public void enableFollowLocation() { super.enableFollowLocation(); callbacks.onEnableFollowMyLocation(); } @Override public void disableFollowLocation() { + super.disableFollowLocation(); callbacks.onDisableFollowMyLocation(); } public interface OverlayCallbacks{ /** * Called right after disableFollowMyLocation */ void onDisableFollowMyLocation(); /** * Called right after enableFollowMyLocation */ void onEnableFollowMyLocation(); } } diff --git a/src/it/reyboz/bustorino/middleware/AppLocationManager.java b/src/it/reyboz/bustorino/middleware/AppLocationManager.java index 059b588..9c5fc07 100644 --- a/src/it/reyboz/bustorino/middleware/AppLocationManager.java +++ b/src/it/reyboz/bustorino/middleware/AppLocationManager.java @@ -1,223 +1,269 @@ /* BusTO (middleware) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.middleware; +import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; import android.util.Log; import android.widget.Toast; + +import androidx.core.content.ContextCompat; + import it.reyboz.bustorino.util.LocationCriteria; +import it.reyboz.bustorino.util.Permissions; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.ListIterator; /** * Singleton class used to access location. Possibly extended with other location sources. */ public class AppLocationManager implements LocationListener { public static final int LOCATION_GPS_AVAILABLE = 22; public static final int LOCATION_UNAVAILABLE = -22; - private Context con; - private LocationManager locMan; + private final Context appContext; + private final LocationManager locMan; public static final String DEBUG_TAG = "BUSTO LocAdapter"; private final String BUNDLE_LOCATION = "location"; private static AppLocationManager instance; private int oldGPSLocStatus = LOCATION_UNAVAILABLE; private int minimum_time_milli = -1; - private ArrayList> requestersRef = new ArrayList<>(); + private boolean isLocationPermissionGiven = false; + + private final ArrayList> requestersRef = new ArrayList<>(); - private AppLocationManager(Context con) { - this.con = con.getApplicationContext(); - locMan = (LocationManager) con.getSystemService(Context.LOCATION_SERVICE); + private AppLocationManager(Context context) { + this.appContext = context.getApplicationContext(); + locMan = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + isLocationPermissionGiven = checkLocationPermission(context); } public static AppLocationManager getInstance(Context con) { - if(instance==null) instance = new AppLocationManager(con.getApplicationContext()); + if(instance==null) instance = new AppLocationManager(con); return instance; } + + public static boolean checkLocationPermission(Context context){ + return ContextCompat.checkSelfPermission(context, + Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED; + } - private void requestGPSPositionUpdates(){ + private void requestGPSPositionUpdates() throws SecurityException{ final int timeinterval = (minimum_time_milli>0 && minimum_time_milli> iter = requestersRef.listIterator(); while(iter.hasNext()){ final LocationRequester cReq = iter.next().get(); if(cReq==null) iter.remove(); else { minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); } } Log.d(DEBUG_TAG,"Updated requesters, got "+requestersRef.size()+" listeners to update every "+minimum_time_milli+" ms at least"); } public void addLocationRequestFor(LocationRequester req){ boolean present = false; minimum_time_milli = Integer.MAX_VALUE; + int countNull = 0; ListIterator> iter = requestersRef.listIterator(); while(iter.hasNext()){ final LocationRequester cReq = iter.next().get(); - if(cReq==null) iter.remove(); - else if(cReq.equals(req)){ + if(cReq==null) { + countNull++; + iter.remove(); + } else if(cReq.equals(req)){ present = true; minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); } } + Log.d(DEBUG_TAG, countNull+" listeners have been removed because null"); if(!present) { WeakReference newref = new WeakReference<>(req); requestersRef.add(newref); minimum_time_milli = Math.min(req.getLocationCriteria().getTimeInterval(),minimum_time_milli); Log.d(DEBUG_TAG,"Added new stop requester, instance of "+req.getClass().getSimpleName()); } if(requestersRef.size()>0){ - Log.d(DEBUG_TAG,"Requesting position updates"); + Log.d(DEBUG_TAG,"Requesting location updates"); requestGPSPositionUpdates(); } } public void removeLocationRequestFor(LocationRequester req){ minimum_time_milli = Integer.MAX_VALUE; ListIterator> iter = requestersRef.listIterator(); while(iter.hasNext()){ final LocationRequester cReq = iter.next().get(); if(cReq==null || cReq.equals(req)) iter.remove(); else { minimum_time_milli = Math.min(cReq.getLocationCriteria().getTimeInterval(),minimum_time_milli); } } if(requestersRef.size()<=0){ locMan.removeUpdates(this); } else { requestGPSPositionUpdates(); } } private void sendLocationStatusToAll(int status){ ListIterator> iter = requestersRef.listIterator(); while(iter.hasNext()){ final LocationRequester cReq = iter.next().get(); if(cReq==null) iter.remove(); else cReq.onLocationStatusChanged(status); } } + public boolean isRequesterRegistered(LocationRequester requester){ + for(WeakReference regRef: requestersRef){ + if(regRef.get()!=null && regRef.get() ==requester) return true; + } + return false; + } @Override public void onLocationChanged(Location location) { Log.d(DEBUG_TAG,"found location:\nlat: "+location.getLatitude()+" lon: "+location.getLongitude()+"\naccuracy: "+location.getAccuracy()); ListIterator> iter = requestersRef.listIterator(); int new_min_interval = Integer.MAX_VALUE; while(iter.hasNext()){ final LocationRequester requester = iter.next().get(); if(requester==null) iter.remove(); else{ final long timeNow = System.currentTimeMillis(); final LocationCriteria criteria = requester.getLocationCriteria(); if(location.getAccuracy()criteria.getTimeInterval()){ requester.onLocationChanged(location); Log.d("AppLocationManager","Updating position for instance of requester "+requester.getClass().getSimpleName()); } //update minimum time interval new_min_interval = Math.min(requester.getLocationCriteria().getTimeInterval(),new_min_interval); } } minimum_time_milli = new_min_interval; if(requestersRef.size()==0){ //stop requesting the position locMan.removeUpdates(this); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { //IF ANOTHER LOCATION SOURCE IS READY, USE IT //OTHERWISE, SIGNAL THAT WE HAVE NO LOCATION if(oldGPSLocStatus !=status){ if(status == LocationProvider.OUT_OF_SERVICE || status == LocationProvider.TEMPORARILY_UNAVAILABLE) { sendLocationStatusToAll(LOCATION_UNAVAILABLE); }else if(status == LocationProvider.AVAILABLE){ sendLocationStatusToAll(LOCATION_GPS_AVAILABLE); } oldGPSLocStatus = status; } - Log.d(DEBUG_TAG, "Provider: "+provider+" status: "+status); + Log.d(DEBUG_TAG, "Provider status changed: "+provider+" status: "+status); } @Override public void onProviderEnabled(String provider) { + cleanAndUpdateRequesters(); requestGPSPositionUpdates(); Log.d(DEBUG_TAG, "Provider: "+provider+" enabled"); + for(WeakReference req: requestersRef){ + if(req.get()==null) continue; + req.get().onLocationProviderAvailable(); + } + } @Override public void onProviderDisabled(String provider) { - locMan.removeUpdates(this); + cleanAndUpdateRequesters(); + for(WeakReference req: requestersRef){ + if(req.get()==null) continue; + req.get().onLocationDisabled(); + } + //locMan.removeUpdates(this); Log.d(DEBUG_TAG, "Provider: "+provider+" disabled"); } + public boolean anyLocationProviderMatchesCriteria(Criteria cr) { + return Permissions.anyLocationProviderMatchesCriteria(locMan, cr, true); + } /** * Interface to be implemented to get the location request */ public interface LocationRequester{ /** * Do something with the newly obtained location * @param loc the obtained location */ void onLocationChanged(Location loc); /** * Inform the requester that the GPS status has changed * @param status new status */ void onLocationStatusChanged(int status); + /** + * We have a location provider available + */ + void onLocationProviderAvailable(); + /** + * Called when location is disabled + */ + void onLocationDisabled(); + + /** * Give the last time of update the requester has * Set it to -1 in order to receive each new location * @return the time for update in milliseconds since epoch */ long getLastUpdateTimeMillis(); /** * Get the specifications for the location * @return fully parsed LocationCriteria */ LocationCriteria getLocationCriteria(); } } diff --git a/src/it/reyboz/bustorino/middleware/GeneralActivity.java b/src/it/reyboz/bustorino/middleware/GeneralActivity.java index 5bfb4dc..4ace9f4 100644 --- a/src/it/reyboz/bustorino/middleware/GeneralActivity.java +++ b/src/it/reyboz/bustorino/middleware/GeneralActivity.java @@ -1,124 +1,154 @@ package it.reyboz.bustorino.middleware; import android.Manifest; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.graphics.Rect; import android.net.ConnectivityManager; import android.net.NetworkInfo; import com.google.android.material.snackbar.Snackbar; + +import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import java.util.HashMap; import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.utils; /** * Activity class that contains all the generally useful methods */ public abstract class GeneralActivity extends AppCompatActivity { final static protected int PERMISSION_REQUEST_POSITION = 33; final static protected String LOCATION_PERMISSION_GIVEN = "loc_permission"; final static protected int STORAGE_PERMISSION_REQ = 291; final static protected int PERMISSION_OK = 0; final static protected int PERMISSION_ASKING = 11; final static protected int PERMISSION_NEG_CANNOT_ASK = -3; final static private String DEBUG_TAG = "BusTO-GeneralAct"; /* * Permission stuff */ protected HashMap permissionDoneRunnables = new HashMap<>(); protected HashMap permissionAsked = new HashMap<>(); protected void setOption(String optionName, boolean value) { SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { SharedPreferences preferences = getPreferences(MODE_PRIVATE); return preferences.getBoolean(optionName, optDefault); } protected SharedPreferences getMainSharedPreferences(){ return getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE); } public void hideKeyboard() { View view = getCurrentFocus(); if (view != null) { ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Toast.makeText(getApplicationContext(), messageID, length).show(); } public int askForPermissionIfNeeded(String permission, int requestID){ if(ContextCompat.checkSelfPermission(getApplicationContext(),permission)==PackageManager.PERMISSION_GRANTED){ return PERMISSION_OK; } //need to ask for the permission //consider scenario when we have already asked for permission boolean alreadyAsked = false; Integer num_trials = 0; synchronized (this){ if (permissionAsked.containsKey(permission)){ num_trials = permissionAsked.get(permission); - if (num_trials != null && num_trials > 3) + if (num_trials != null && num_trials > 4) alreadyAsked = true; } } Log.d(DEBUG_TAG,"Already asked for permission: "+permission+" -> "+num_trials); if(!alreadyAsked){ ActivityCompat.requestPermissions(this,new String[]{permission}, requestID); synchronized (this){ if (num_trials!=null){ permissionAsked.put(permission, num_trials+1); } } return PERMISSION_ASKING; } else { return PERMISSION_NEG_CANNOT_ASK; } } public void createSnackbar(int ViewID, String message,int duration){ Snackbar.make(findViewById(ViewID),message,duration); } /* METHOD THAT MIGHT BE USEFUL LATER public void assertPermissions(String[] permissions){ ArrayList permissionstoRequest = new ArrayList<>(); for(int i=0;i marginOfError; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + } } diff --git a/src/it/reyboz/bustorino/util/LocationCriteria.java b/src/it/reyboz/bustorino/util/LocationCriteria.java index 4dd3282..ae2ac8f 100644 --- a/src/it/reyboz/bustorino/util/LocationCriteria.java +++ b/src/it/reyboz/bustorino/util/LocationCriteria.java @@ -1,41 +1,49 @@ /* BusTO (util) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.util; +import android.location.Criteria; + /** * Own Location Criteria, because it's fun + * */ -public class LocationCriteria { +public class LocationCriteria extends Criteria { private final float minAccuracy; private final int timeInterval; + /** + * Constructor + * @param minAccuracy in meters + * @param timeInterval in milliseconds + */ public LocationCriteria(float minAccuracy,int timeInterval){ this.minAccuracy = minAccuracy; this.timeInterval = timeInterval; } public float getMinAccuracy() { return minAccuracy; } public int getTimeInterval() { return timeInterval; } } diff --git a/src/it/reyboz/bustorino/util/Permissions.java b/src/it/reyboz/bustorino/util/Permissions.java index 984edf2..487eb04 100644 --- a/src/it/reyboz/bustorino/util/Permissions.java +++ b/src/it/reyboz/bustorino/util/Permissions.java @@ -1,41 +1,56 @@ package it.reyboz.bustorino.util; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.LocationManager; import android.util.Log; +import androidx.activity.result.ActivityResultCaller; +import androidx.activity.result.ActivityResultLauncher; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; import java.util.List; public class Permissions { final static public String DEBUG_TAG = "BusTO -Permissions"; final static public int PERMISSION_REQUEST_POSITION = 33; final static public String LOCATION_PERMISSION_GIVEN = "loc_permission"; final static public int STORAGE_PERMISSION_REQ = 291; final static public int PERMISSION_OK = 0; final static public int PERMISSION_ASKING = 11; final static public int PERMISSION_NEG_CANNOT_ASK = -3; + final static public String[] LOCATION_PERMISSIONS={Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION}; + public static boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) { List 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; } + public static boolean isPermissionGranted(Context con,String permission){ + return ContextCompat.checkSelfPermission(con, permission) == PackageManager.PERMISSION_GRANTED; + } + + public static boolean locationPermissionGranted(Context con){ + return isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) && + isPermissionGranted(con, Manifest.permission.ACCESS_COARSE_LOCATION); + } public static void assertLocationPermissions(Context con, Activity activity) { - if(ContextCompat.checkSelfPermission(con, Manifest.permission.ACCESS_FINE_LOCATION)!= PackageManager.PERMISSION_GRANTED){ + if(!isPermissionGranted(con, Manifest.permission.ACCESS_FINE_LOCATION) || + !isPermissionGranted(con,Manifest.permission.ACCESS_COARSE_LOCATION)){ ActivityCompat.requestPermissions(activity,new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_POSITION); } } }