diff --git a/AndroidManifest.xml b/AndroidManifest.xml --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -15,10 +15,12 @@ + android:name=".BustoApp" + android:networkSecurityConfig="@xml/networks_security_config" + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme"> + + diff --git a/res/layout-v11/activity_main.xml b/res/layout-v11/activity_main.xml --- a/res/layout-v11/activity_main.xml +++ b/res/layout-v11/activity_main.xml @@ -1,5 +1,4 @@ - - - + - - + - - - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/fragment_arrivals.xml b/res/layout/fragment_arrivals.xml new file mode 100644 --- /dev/null +++ b/res/layout/fragment_arrivals.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/fragment_list_view.xml b/res/layout/fragment_list_view.xml --- a/res/layout/fragment_list_view.xml +++ b/res/layout/fragment_list_view.xml @@ -4,45 +4,40 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + + android:minHeight="48dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_toStartOf="@+id/addToFavorites" + android:foreground="?attr/selectableItemBackground" /> - + + android:visibility="visible" > + + + \ No newline at end of file diff --git a/res/layout/fragment_nearby_stops.xml b/res/layout/fragment_nearby_stops.xml --- a/res/layout/fragment_nearby_stops.xml +++ b/res/layout/fragment_nearby_stops.xml @@ -14,7 +14,7 @@ android:layout_toLeftOf="@id/switchButton" android:layout_marginLeft="10dp" android:layout_marginStart="10dp" android:layout_marginRight="10dp" android:layout_marginEnd="10dp" /> - - - - \ No newline at end of file + \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -30,13 +30,16 @@ Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa - Nessun preferito? Arghh! Schiaccia sulla stella di una fermata per aggiungerla a questa lista! + 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. + 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! 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 + diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -46,6 +46,7 @@ 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! @@ -126,4 +127,25 @@ 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 crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n diff --git a/res/xml/networks_security_config.xml b/res/xml/networks_security_config.xml new file mode 100644 --- /dev/null +++ b/res/xml/networks_security_config.xml @@ -0,0 +1,8 @@ + + + + 5t.torino.it + gtt.to.it + + + \ No newline at end of file diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,15 +1,15 @@ - - - - - + - - - + - + diff --git a/src/it/reyboz/bustorino/ActivityAbout.java b/src/it/reyboz/bustorino/ActivityAbout.java --- a/src/it/reyboz/bustorino/ActivityAbout.java +++ b/src/it/reyboz/bustorino/ActivityAbout.java @@ -17,9 +17,9 @@ */ package it.reyboz.bustorino; -import android.support.v4.app.NavUtils; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; +import androidx.core.app.NavUtils; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; diff --git a/src/it/reyboz/bustorino/ActivityFavorites.java b/src/it/reyboz/bustorino/ActivityFavorites.java --- a/src/it/reyboz/bustorino/ActivityFavorites.java +++ b/src/it/reyboz/bustorino/ActivityFavorites.java @@ -18,8 +18,8 @@ package it.reyboz.bustorino; import android.database.Cursor; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import android.widget.*; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.adapters.StopAdapter; @@ -30,11 +30,10 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.net.Uri; import android.os.AsyncTask; -import android.support.v4.app.NavUtils; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; +import androidx.core.app.NavUtils; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; @@ -46,8 +45,6 @@ import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; -import org.osmdroid.util.GeoPoint; - import java.util.List; public class ActivityFavorites extends AppCompatActivity implements LoaderManager.LoaderCallbacks { diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -17,23 +17,37 @@ */ package it.reyboz.bustorino; -import android.annotation.SuppressLint; +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.database.sqlite.SQLiteDatabase; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.NonNull; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.NavUtils; -import android.support.v4.widget.SwipeRefreshLayout; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +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.KeyEvent; import android.view.Menu; @@ -45,13 +59,16 @@ import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -import android.support.design.widget.FloatingActionButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; public class ActivityMain extends GeneralActivity implements FragmentListener { @@ -82,12 +99,7 @@ * Options */ private final String OPTION_SHOW_LEGEND = "show_legend"; - private final String LOCATION_PERMISSION_GIVEN = "loc_permission"; - /* - * Status - */ - private DBStatusManager prefsManager; - private DBStatusManager.OnDBUpdateStatusChangeListener updatelistener; + private static final String DEBUG_TAG = "BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @@ -99,9 +111,8 @@ } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ - - private RecursionHelper ArrivalFetchersRecursionHelper = new RecursionHelper<>(new ArrivalsFetcher[]{new GTTJSONFetcher(), new FiveTScraperFetcher()}); - private RecursionHelper StopsFindersByNameRecursionHelper = new RecursionHelper<>(new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}); + private ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + private StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; /* * Position */ @@ -109,11 +120,6 @@ private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; - /* - * Database Access - */ - private StopsDB stopsDB; - private UserDB userDB; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// @@ -121,78 +127,64 @@ /* * @see swipeRefreshLayout */ - private Handler handler = new Handler(); + private final Handler theHandler = new Handler(); private final Runnable refreshing = new Runnable() { public void run() { if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); String stopName = fragment.getStopID(); - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(stopName); - } else - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(); + + new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray()).execute(stopName); + } else //we create a new fragment, which is WRONG + new AsyncDataDownload(fh, arrivalsFetchers).execute(); } }; + //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); - this.stopsDB = new StopsDB(getApplicationContext()); - this.userDB = new UserDB(getApplicationContext()); + final SharedPreferences theShPr = getMainSharedPreferences(); + /* + * Database Access + */ setContentView(R.layout.activity_main); - busStopSearchByIDEditText = (EditText) findViewById(R.id.busStopSearchByIDEditText); - busStopSearchByNameEditText = (EditText) findViewById(R.id.busStopSearchByNameEditText); - progressBar = (ProgressBar) findViewById(R.id.progressBar); - howDoesItWorkTextView = (TextView) findViewById(R.id.howDoesItWorkTextView); - hideHintButton = (Button) findViewById(R.id.hideHintButton); - swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.listRefreshLayout); - floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton); - - framan.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { - @Override - public void onBackStackChanged() { - Log.d("MainActivity, BusTO", "BACK STACK CHANGED"); - } - }); + busStopSearchByIDEditText = 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(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, - KeyEvent event) { - // IME_ACTION_SEARCH alphabetical option - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - onSearchClick(v); - return true; - } - return false; + .setOnEditorActionListener((v, actionId, event) -> { + // IME_ACTION_SEARCH alphabetical option + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + onSearchClick(v); + return true; } + return false; }); busStopSearchByNameEditText - .setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, - KeyEvent event) { - // IME_ACTION_SEARCH alphabetical option - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - onSearchClick(v); - return true; - } - return false; + .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(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - handler.post(refreshing); - } - }); + .setOnRefreshListener(() -> theHandler.post(refreshing)); /** * @author Marco Gagino!!! @@ -241,7 +233,6 @@ if (tryedFromIntent) { // This shows a luser warning - ArrivalFetchersRecursionHelper.reset(); Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } @@ -260,32 +251,54 @@ createFragmentForStop(busStopID); } //Try (hopefully) database update - //TODO: Start the service in foreground, check last time it ran before - DatabaseUpdateService.startDBUpdate(getApplicationContext()); + //TODO: Check if service shows the notification + //Old code for the db update + //DatabaseUpdateService.startDBUpdate(getApplicationContext()); + + + 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 */ - updatelistener = new DBStatusManager.OnDBUpdateStatusChangeListener() { - @Override - public boolean defaultStatusValue() { - return true; - } + 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); - @Override - public void onDBStatusChanged(boolean updating) { + boolean showProgress = false; + for (WorkInfo workInfo : workInfoList) { + if (workInfo.getState() == WorkInfo.State.RUNNING) { + showProgress = true; + } + } - if (updating) { - createDefaultSnackbar(); - } else if (snackbar != null) { - snackbar.dismiss(); - snackbar = null; - } + if (showProgress) { + createDefaultSnackbar(); + } else { + if(snackbar!=null) { + snackbar.dismiss(); + snackbar = null; + } + } + + }); - } - }; - prefsManager = new DBStatusManager(getApplicationContext(), updatelistener); - prefsManager.registerListener(); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// @@ -297,7 +310,7 @@ cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); //We want the nearby bus stops! - handler.post(new NearbyStopsRequester()); + theHandler.post(new NearbyStopsRequester()); //If there are no providers available, then, wait for them @@ -314,7 +327,6 @@ * Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop()); * if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) { * setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID); - * //new asyncWgetBusStopFromBusStopID(lastSuccessfullySearchedBusStop.ID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); * new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); * } else { * //we have new activity or we don't have a new searched stop. @@ -336,7 +348,6 @@ super.onPause(); fh.stopLastRequestIfNeeded(); fh.setBlockAllActivities(true); - if (updatelistener != null && prefsManager != null) prefsManager.unregisterListener(); locmgr.removeUpdates(locListener); } @@ -344,14 +355,9 @@ protected void onResume() { super.onResume(); fh.setBlockAllActivities(false); - if (updatelistener != null && prefsManager != null) { - prefsManager.registerListener(); - if (prefsManager.isDBUpdating(true)) { - createDefaultSnackbar(); - } - } + //TODO: check if current LiveData-bound observer works if (pendingNearbyStopsRequest) - handler.post(new NearbyStopsRequester()); + theHandler.post(new NearbyStopsRequester()); } @Override @@ -386,8 +392,25 @@ startActivity(new Intent(ActivityMain.this, ActivityFavorites.class)); return true; case R.id.action_map: - startActivity(new Intent(ActivityMain.this, ActivityMap.class)); + //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: + Resources res = getResources(); + 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; @@ -418,20 +441,11 @@ public void onSearchClick(View v) { if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); - //OLD ASYNCTASK - //new asyncWgetBusStopFromBusStopID(busStopID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); - - if (busStopID == null || busStopID.length() <= 0) { - showMessage(R.string.insert_bus_stop_number_error); - toggleSpinner(false); - } else { - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(busStopID); - Log.d("MainActiv", "Started search for arrivals of stop " + busStopID); - } + createFragmentForStop(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); - new AsyncDataDownload(AsyncDataDownload.RequestType.STOPS, fh).execute(query); + new AsyncDataDownload(fh, stopsFinderByNames).execute(query); } } @@ -447,7 +461,7 @@ //if we sent a request for a new NearbyStopsFragment if (pendingNearbyStopsRequest) { pendingNearbyStopsRequest = false; - handler.post(new NearbyStopsRequester()); + theHandler.post(new NearbyStopsRequester()); } } else { @@ -455,6 +469,31 @@ 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); + }*/ + } } } @@ -462,13 +501,22 @@ @Override public void createFragmentForStop(String ID) { - //new asyncWgetBusStopFromBusStopID(ID, ArrivalFetchersRecursionHelper,lastSuccessfullySearchedBusStop); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress - showMessage(R.string.insert_bus_stop_number_error); + showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); - } else { - new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS, fh).execute(ID); + } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { + ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); + if (fragment.getStopID() != null && fragment.getStopID().equals(ID)){ + // Run with previous fetchers + //fragment.getCurrentFetchers().toArray() + new AsyncDataDownload(fh,fragment.getCurrentFetchersAsArray()).execute(ID); + } else{ + new AsyncDataDownload(fh, arrivalsFetchers).execute(ID); + } + } + else { + new AsyncDataDownload(fh,arrivalsFetchers).execute(ID); Log.d("MainActiv", "Started search for arrivals of stop " + ID); } } @@ -488,6 +536,7 @@ * 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; @@ -535,7 +584,7 @@ Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { pendingNearbyStopsRequest = false; - handler.post(new NearbyStopsRequester()); + theHandler.post(new NearbyStopsRequester()); } } @@ -564,16 +613,27 @@ } }; + /** + * Run location requests separately and asynchronously + */ class NearbyStopsRequester implements Runnable { - @SuppressLint("MissingPermission") @Override public void run() { final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); - - if (!canRunPosition) { - pendingNearbyStopsRequest = true; - assertLocationPermissions(); - return; + 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; + + //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; + assertLocationPermissions(); + 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); @@ -581,7 +641,9 @@ Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); return; } - if (anyLocationProviderMatchesCriteria(locManager, cr, true) && fh.getLastSuccessfullySearchedBusStop() == null) { + 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); @@ -598,6 +660,7 @@ //Wait for the providers Log.d(DEBUG_TAG, "Queuing position request"); pendingNearbyStopsRequest = true; + locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, locListener); } @@ -726,18 +789,12 @@ } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// - @Override public void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } - @Override - public void showMessage(int messageID) { - Toast.makeText(getApplicationContext(), messageID, Toast.LENGTH_SHORT).show(); - } - private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); diff --git a/src/it/reyboz/bustorino/ActivityMap.java b/src/it/reyboz/bustorino/ActivityMap.java --- a/src/it/reyboz/bustorino/ActivityMap.java +++ b/src/it/reyboz/bustorino/ActivityMap.java @@ -19,8 +19,10 @@ package it.reyboz.bustorino; +import android.Manifest; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationManager; import android.os.Build; @@ -28,12 +30,16 @@ import android.util.Log; import android.view.View; import android.widget.ImageButton; -import android.support.annotation.RequiresApi; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.preference.PreferenceManager; -import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.preference.PreferenceManager; + +import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.middleware.NextGenDB; + import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; @@ -55,9 +61,8 @@ import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.map.CustomInfoWindow; -import it.reyboz.bustorino.middleware.StopsDB; -public class ActivityMap extends AppCompatActivity { +public class ActivityMap extends GeneralActivity { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; @@ -84,12 +89,15 @@ protected ImageButton btFollowMe; @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) - @Override public void onCreate(Bundle savedInstanceState) { + @Override + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //handle permissions first, before map is created. not depicted here - //load/initialize the osmdroid configuration, this can be done + //load/initialize the osmdroid configuration + + ctx = getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); //setting this before the layout is inflated is a good idea @@ -138,7 +146,6 @@ })); - btCenterMap.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -165,12 +172,13 @@ } + public void startMap(Bundle incoming, Bundle savedInstanceState) { //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; - if(incoming != null) { + if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); @@ -184,11 +192,17 @@ IMapController mapController = map.getController(); GeoPoint startPoint = null; + boolean havePositionPermission = true; + + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, PERMISSION_REQUEST_POSITION); + havePositionPermission = false; + } + if (marker != null) { startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); - } - else if (savedInstanceState != null) { + } else if (savedInstanceState != null || !havePositionPermission) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); @@ -196,7 +210,8 @@ boolean found = false; LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); - if(locationManager!=null) { + if (locationManager != null) { + Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); @@ -390,4 +405,25 @@ outState.putDouble(MAP_CENTER_LON_KEY, map.getMapCenter().getLongitude()); } + /** + * PERMISSION STUFF + **/ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case PERMISSION_REQUEST_POSITION: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + setOption(LOCATION_PERMISSION_GIVEN, true); + //if we sent a request for a new NearbyStopsFragment + + + } else { + //permission denied + setOption(LOCATION_PERMISSION_GIVEN, false); + } + break; + //add other cases for permissions + } + + } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivitySettings.java b/src/it/reyboz/bustorino/ActivitySettings.java --- a/src/it/reyboz/bustorino/ActivitySettings.java +++ b/src/it/reyboz/bustorino/ActivitySettings.java @@ -1,10 +1,10 @@ package it.reyboz.bustorino; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import it.reyboz.bustorino.fragments.SettingsFragment; diff --git a/src/it/reyboz/bustorino/BustoApp.java b/src/it/reyboz/bustorino/BustoApp.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/BustoApp.java @@ -0,0 +1,36 @@ +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.config.CoreConfigurationBuilder; +import org.acra.config.DialogConfigurationBuilder; +import org.acra.config.MailSenderConfigurationBuilder; +import org.acra.data.StringFormat; + + +public class BustoApp extends Application { + + @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); + + ACRA.init(this, builder); + } +} diff --git a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java --- a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -19,9 +19,9 @@ import android.content.Context; import android.location.Location; -import android.support.annotation.Nullable; -import android.support.v4.util.Pair; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java --- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.adapters; import android.content.Context; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; diff --git a/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java --- a/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java @@ -17,10 +17,9 @@ */ package it.reyboz.bustorino.adapters; -import android.content.Context; import android.location.Location; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; diff --git a/src/it/reyboz/bustorino/adapters/StopAdapter.java b/src/it/reyboz/bustorino/adapters/StopAdapter.java --- a/src/it/reyboz/bustorino/adapters/StopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopAdapter.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.adapters; import android.content.Context; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java --- a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java +++ b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java @@ -49,4 +49,10 @@ * @see FiveTNormalizer */ Palina ReadArrivalTimesAll(String stopID, AtomicReference res); + + /** + * Get the determined source for the Fetcher + * @return the source of the arrival times + */ + Passaggio.Source getSourceForFetcher(); } diff --git a/src/it/reyboz/bustorino/backend/DBStatusManager.java b/src/it/reyboz/bustorino/backend/DBStatusManager.java --- a/src/it/reyboz/bustorino/backend/DBStatusManager.java +++ b/src/it/reyboz/bustorino/backend/DBStatusManager.java @@ -46,7 +46,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Log.d("BUSTO-PrefListener", "Changed key " + key + " in the sharedPref"); - if (key.equals(DB_UPDATING)) { + if (dbUpdateListener!=null && key.equals(DB_UPDATING)) { dbUpdateListener.onDBStatusChanged(sharedPreferences.getBoolean(DB_UPDATING, dbUpdateListener.defaultStatusValue())); } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -17,7 +17,7 @@ */ package it.reyboz.bustorino.backend; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; @@ -27,8 +27,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; @@ -68,6 +66,11 @@ return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.FiveTAPI; + } + List parseArrivalsServerResponse(String JSONresponse, AtomicReference res) throws JSONException{ ArrayList routes = new ArrayList<>(3); /* diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java --- a/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java @@ -18,14 +18,13 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import com.android.volley.*; import com.android.volley.toolbox.HttpHeaderParser; import org.json.JSONException; import java.io.UnsupportedEncodingException; -import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; diff --git a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java --- a/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTScraperFetcher.java @@ -159,7 +159,7 @@ // Yes... Sometimes there is an EMPTY td ._. continue; } - p.addPassaggio(time, Passaggio.Source.FiveTScraper,routeIndex); + p.addPassaggio(time, Passaggio.Source.FiveTScraper, routeIndex); } } @@ -168,6 +168,11 @@ return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.FiveTScraper; + } + // preserved for future generations: // /* // * I've sent many emails to the public email info@5t.torino.it to write down something like: diff --git a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java --- a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; @@ -89,7 +89,11 @@ passaggi = thisroute.getJSONArray("PassaggiRT"); howManyPassaggi = passaggi.length(); for(j = 0; j < howManyPassaggi; j++) { - p.addPassaggio(passaggi.getString(j).concat("*"), Passaggio.Source.GTTJSON, pos); + String mPassaggio = passaggi.getString(j); + if (mPassaggio.contains("__")){ + mPassaggio = mPassaggio.replace("_", ""); + } + p.addPassaggio(mPassaggio.concat("*"), Passaggio.Source.GTTJSON, pos); } passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones @@ -108,4 +112,9 @@ return p; } + @Override + public Passaggio.Source getSourceForFetcher() { + return Passaggio.Source.GTTJSON; + } + } diff --git a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java --- a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; diff --git a/src/it/reyboz/bustorino/backend/Notifications.java b/src/it/reyboz/bustorino/backend/Notifications.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/Notifications.java @@ -0,0 +1,46 @@ +package it.reyboz.bustorino.backend; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import it.reyboz.bustorino.R; + +public class Notifications { + public static final String DEFAULT_CHANNEL_ID ="Default"; + + public static void createDefaultNotificationChannel(Context context) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = context.getString(R.string.default_notification_channel); + String description = context.getString(R.string.default_notification_channel_description); + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + /** + * Register a notification channel on Android Oreo and above + * @param con a Context + * @param name channel name + * @param description channel description + * @param importance channel importance (from NotificationManager) + * @param ID channel ID + */ + public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(ID, name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = con.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } +} diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -33,6 +33,8 @@ */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); + private boolean routesModified = false; + private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); @@ -52,6 +54,7 @@ */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); + routesModified = true; } /** @@ -76,11 +79,13 @@ * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { - this.routes.add(new Route(routeID, destinazione, type, new ArrayList(6))); + this.routes.add(new Route(routeID, destinazione, type, new ArrayList<>(6))); + routesModified = true; return this.routes.size() - 1; // last inserted element and pray that direct access to ArrayList elements really is direct } public int addRoute(Route r){ this.routes.add(r); + routesModified = true; return this.routes.size()-1; } public void setRoutes(List routeList){ @@ -151,6 +156,35 @@ // public List queryRouteByIndex(int index) { // return this.routes.get(index).getPassaggi(); // } + protected void checkPassaggi(){ + Passaggio.Source mSource = null; + for (Route r: routes){ + for(Passaggio pass: r.passaggi){ + if (mSource == null) { + mSource = pass.source; + } else if (mSource != pass.source){ + Log.w("BusTO-CheckPassaggi", + "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); + mSource = Passaggio.Source.UNDETERMINED; + + break; + } + } + if(mSource == Passaggio.Source.UNDETERMINED) + break; + } + //finished with the check, setting flags + routesModified = false; + allSource = mSource; + } + + public Passaggio.Source getPassaggiSourceIfAny(){ + if(allSource==null || routesModified){ + checkPassaggi(); + } + assert allSource != null; + return allSource; + } /** * Gets every route and its timetable. @@ -234,8 +268,6 @@ return count; } - - // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java --- a/src/it/reyboz/bustorino/backend/Passaggio.java +++ b/src/it/reyboz/bustorino/backend/Passaggio.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.util.Log; public final class Passaggio implements Comparable { @@ -29,7 +29,7 @@ private final String passaggioGTT; public final int hh,mm; public final boolean isInRealTime; - public final Source passageSource; + public final Source source; /** @@ -55,7 +55,7 @@ */ public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) { passaggioGTT = TimeGTT; - passageSource = sorgente; + source = sorgente; String[] parts = TimeGTT.split(":"); String hh,mm; boolean realtime; @@ -95,7 +95,7 @@ this.hh = hour; this.mm = minutes; this.isInRealTime = realtime; - this.passageSource = sorgente; + this.source = sorgente; //Build the passaggio string StringBuilder sb = new StringBuilder(); sb.append(hour).append(":").append(minutes); @@ -154,6 +154,6 @@ // } // } public enum Source{ - FiveTAPI,GTTJSON,FiveTScraper + FiveTAPI,GTTJSON,FiveTScraper, UNDETERMINED } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java --- a/src/it/reyboz/bustorino/backend/Route.java +++ b/src/it/reyboz/bustorino/backend/Route.java @@ -18,8 +18,8 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -19,9 +19,9 @@ package it.reyboz.bustorino.backend; import android.location.Location; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import it.reyboz.bustorino.util.LinesNameSorter; import java.net.URLEncoder; diff --git a/src/it/reyboz/bustorino/backend/StopsDBInterface.java b/src/it/reyboz/bustorino/backend/StopsDBInterface.java --- a/src/it/reyboz/bustorino/backend/StopsDBInterface.java +++ b/src/it/reyboz/bustorino/backend/StopsDBInterface.java @@ -18,8 +18,8 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.List; diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java --- a/src/it/reyboz/bustorino/backend/networkTools.java +++ b/src/it/reyboz/bustorino/backend/networkTools.java @@ -18,7 +18,7 @@ package it.reyboz.bustorino.backend; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import java.io.BufferedInputStream; diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,7 +1,6 @@ package it.reyboz.bustorino.backend; import android.content.Context; -import android.support.v7.widget.RecyclerView; import android.view.View; public abstract class utils { diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -21,16 +21,40 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; + +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.middleware.AppDataProvider; import it.reyboz.bustorino.middleware.NextGenDB; import it.reyboz.bustorino.middleware.UserDB; @@ -42,27 +66,37 @@ private final static String DEBUG_TAG = "BUSTOArrivalsFragment"; 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 TextView messageTextView; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; - private ImageButton addToFavorites; + private Palina lastUpdatedPalina = null; + private boolean needUpdateOnAttach = false; + private boolean requestedNewArrivalTimes = false; + + //Views + protected ImageButton addToFavorites; + protected TextView timesSourceTextView; + + private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); 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); - ArrivalsFragment fragment = new ArrivalsFragment(); //parameter for ResultListFragment args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); - fragment.setArguments(args); - return fragment; - } - public static ArrivalsFragment newInstance(String stopID,String stopName){ - ArrivalsFragment fragment = newInstance(stopID); - Bundle args = fragment.getArguments(); - args.putString(KEY_STOP_NAME,stopName); + if (stopName != null){ + args.putString(KEY_STOP_NAME,stopName); + } fragment.setArguments(args); return fragment; } @@ -73,12 +107,12 @@ stopID = getArguments().getString(KEY_STOP_ID); //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); - final ArrivalsFragment f = this; + final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ - getLoaderManager().restartLoader(loaderFavId,getArguments(),f); + getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); @@ -96,6 +130,65 @@ } + @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(!requestedNewArrivalTimes){ + rotateFetchers(); + timesSourceTextView.setText(R.string.arrival_source_changing); + mListener.createFragmentForStop(stopID); + requestedNewArrivalTimes = 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 + mListener.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); + 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(); @@ -116,11 +209,98 @@ } } + @Override + public void onStart() { + super.onStart(); + if (needUpdateOnAttach){ + updateFragmentData(null); + } + } + @Nullable public String getStopID() { return stopID; } + /** + * 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(); + + 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); + } + final String base_message = getString(R.string.times_source_fmt, source_txt); + timesSourceTextView.setVisibility(View.VISIBLE); + timesSourceTextView.setText(base_message); + requestedNewArrivalTimes = false; + } + + @Override + public void setNewListAdapter(ListAdapter adapter) { + throw new UnsupportedOperationException(); + } + /** * Update the message in the fragment * diff --git a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java --- a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java +++ b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java @@ -17,7 +17,7 @@ */ package it.reyboz.bustorino.fragments; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.widget.AbsListView; diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -21,10 +21,10 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.database.sqlite.SQLiteException; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.widget.SwipeRefreshLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; @@ -65,7 +65,7 @@ /** * Get the last successfully searched bus stop or NULL * - * @return + * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; @@ -92,7 +92,6 @@ return; } - SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID); FragmentManager fm = act.getSupportFragmentManager(); if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { @@ -118,8 +117,9 @@ Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame); } + // DO NOT CALL `setListAdapter` ever on arrivals fragment + arrivalsFragment.updateFragmentData(p); - arrivalsFragment.setListAdapter(new PalinaAdapter(act.getApplicationContext(),p)); act.hideKeyboard(); toggleSpinner(false); } @@ -206,25 +206,30 @@ case OK: break; case CLIENT_OFFLINE: - act.showMessage(R.string.network_error); + act.showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (act.isConnected()) { - act.showMessage(R.string.parsing_error); + act.showToastMessage(R.string.parsing_error, true); } else { - act.showMessage(R.string.network_error); + act.showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: - act.showMessage(R.string.internal_error); + showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: - act.showMessage(R.string.query_too_short); + showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: - act.showMessage(R.string.no_bus_stop_have_this_name); + showShortToast(R.string.no_bus_stop_have_this_name); break; } } + public void showShortToast(int message){ + if (act!=null) + act.showToastMessage(message,true); + } + } diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -24,16 +24,16 @@ import android.location.Location; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.util.Pair; -import android.support.v7.preference.PreferenceManager; -import android.support.v7.widget.AppCompatButton; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; +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; @@ -574,7 +574,7 @@ getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); - Log.d("BusTO: NearbyLocationListener","can start loader "+ canStartDBQuery); + Log.d("BusTO:NearPositListen","can start loader "+ canStartDBQuery); } @Override diff --git a/src/it/reyboz/bustorino/fragments/ResultListFragment.java b/src/it/reyboz/bustorino/fragments/ResultListFragment.java --- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -23,16 +23,16 @@ import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.widget.SwipeRefreshLayout; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.*; -import android.support.design.widget.FloatingActionButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.FiveTNormalizer; @@ -49,20 +49,19 @@ public class ResultListFragment extends Fragment{ // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER static final String LIST_TYPE = "list-type"; - private static final String STOP_TITLE = "messageExtra"; protected static final String LIST_STATE = "list_state"; - private static final String MESSAGE_TEXT_VIEW = "message_text_view"; + protected static final String MESSAGE_TEXT_VIEW = "message_text_view"; private FragmentKind adapterKind; private boolean adapterSet = false; protected FragmentListener mListener; - private TextView messageTextView; - private ImageButton addToFavorites; + protected TextView messageTextView; + protected ListView resultsListView; + private FloatingActionButton fabutton; - private ListView resultsListView; private ListAdapter mListAdapter = null; boolean listShown; private Parcelable mListInstanceState = null; @@ -87,7 +86,7 @@ Bundle args = new Bundle(); args.putSerializable(LIST_TYPE, listType); if (eventualStopTitle != null) { - args.putString(STOP_TITLE, eventualStopTitle); + args.putString(ArrivalsFragment.STOP_TITLE, eventualStopTitle); } fragment.setArguments(args); return fragment; @@ -107,7 +106,7 @@ /** * Check if the last Bus Stop is in the favorites - * @return + * @return true if it iss */ public boolean isStopInFavorites(String busStopId) { boolean found = false; @@ -126,7 +125,6 @@ Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_list_view, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); - addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); if (adapterKind != null) { resultsListView = (ListView) root.findViewById(R.id.resultsListView); switch (adapterKind) { @@ -167,7 +165,7 @@ } } }); - String displayName = getArguments().getString(STOP_TITLE); + String displayName = getArguments().getString(ArrivalsFragment.STOP_TITLE); setTextViewMessage(String.format( getString(R.string.passages), displayName)); break; @@ -204,7 +202,7 @@ ListAdapter adapter = mListAdapter; mListAdapter = null; - setListAdapter(adapter); + resetListAdapter(adapter); } if (mListInstanceState != null) { Log.d("resultsListView", "trying to restore instance state"); @@ -277,7 +275,7 @@ } } - public void setListAdapter(ListAdapter adapter) { + protected void resetListAdapter(ListAdapter adapter) { boolean hadAdapter = mListAdapter != null; mListAdapter = adapter; if (resultsListView != null) { @@ -285,6 +283,9 @@ resultsListView.setVisibility(View.VISIBLE); } } + public void setNewListAdapter(ListAdapter adapter){ + resetListAdapter(adapter); + } /** * Set the message textView @@ -292,23 +293,6 @@ */ public void setTextViewMessage(String message) { messageTextView.setText(message); - switch (adapterKind) { - case ARRIVALS: - addToFavorites.setClickable(true); - addToFavorites.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // add/remove the stop in the favorites - mListener.toggleLastStopToFavorites(); - } - }); - break; - case STOPS: - addToFavorites.setClickable(false); - addToFavorites.setVisibility(View.INVISIBLE); - break; - } - messageTextView.setVisibility(View.VISIBLE); } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; import it.reyboz.bustorino.R; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { diff --git a/src/it/reyboz/bustorino/fragments/StopListFragment.java b/src/it/reyboz/bustorino/fragments/StopListFragment.java --- a/src/it/reyboz/bustorino/fragments/StopListFragment.java +++ b/src/it/reyboz/bustorino/fragments/StopListFragment.java @@ -20,9 +20,9 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; @@ -70,7 +70,7 @@ LoaderManager loaderManager = getLoaderManager(); if(stopList!=null) { mListAdapter = new StopAdapter(getContext(),stopList); - setListAdapter(mListAdapter); + resetListAdapter(mListAdapter); for (int i = 0; i < stopList.size(); i++) { final Bundle b = new Bundle(); b.putString(KEY_STOP_ID, stopList.get(i).ID); diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -20,12 +20,12 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.AsyncTask; -import android.support.v7.app.AppCompatActivity; + +import androidx.annotation.NonNull; import android.util.Log; -import it.reyboz.bustorino.R; + import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.middleware.NextGenDB.Contract.*; @@ -44,36 +44,40 @@ private static final String TAG = "BusTO-DataDownload"; private boolean failedAll = false; - private AtomicReference res; - private RequestType t; + private final AtomicReference res; + private final RequestType t; private String query; WeakReference helperRef; - private ArrayList otherActivities = new ArrayList<>(); + private final ArrayList otherActivities = new ArrayList<>(); + private final Fetcher[] theFetchers; - public AsyncDataDownload(RequestType type,FragmentHelper fh) { - t = type; + public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers) { + RequestType type; helperRef = new WeakReference<>(fh); - fh.setLastTaskRef(new WeakReference(this)); + fh.setLastTaskRef(new WeakReference<>(this)); res = new AtomicReference<>(); + + theFetchers = fetchers; + if (theFetchers.length < 1){ + throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); + } + if (theFetchers[0] instanceof ArrivalsFetcher){ + type = RequestType.ARRIVALS; + } else if (theFetchers[0] instanceof StopsFinderByName){ + type = RequestType.STOPS; + } else{ + type = null; + } + t = type; + } @Override protected Object doInBackground(String... params) { - RecursionHelper r; + RecursionHelper r = new RecursionHelper<>(theFetchers); boolean success=false; Object result; - switch (t){ - case ARRIVALS: - r = new RecursionHelper<>(new ArrivalsFetcher[] {new FiveTAPIFetcher(),new GTTJSONFetcher(), new FiveTScraperFetcher()}); - break; - case STOPS: - r = new RecursionHelper<>(new StopsFinderByName[] {new GTTStopsFetcher(), new FiveTStopsFetcher()}); - break; - default: - //TODO put error message - return null; - } FragmentHelper fh = helperRef.get(); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ @@ -229,10 +233,10 @@ } public class BranchInserter implements Runnable{ - private List routesToInsert; + private final List routesToInsert; private String stopID; - private FragmentHelper fragmentHelper; + private final FragmentHelper fragmentHelper; public BranchInserter(List routesToInsert,FragmentHelper fh,String stopID) { this.routesToInsert = routesToInsert; diff --git a/src/it/reyboz/bustorino/middleware/DBUpdateWorker.java b/src/it/reyboz/bustorino/middleware/DBUpdateWorker.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/middleware/DBUpdateWorker.java @@ -0,0 +1,139 @@ +package it.reyboz.bustorino.middleware; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.work.*; +import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.Fetcher; +import it.reyboz.bustorino.backend.Notifications; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static android.content.Context.MODE_PRIVATE; + +public class DBUpdateWorker extends Worker{ + + + public static final String ERROR_CODE_KEY ="Error_Code"; + public static final String ERROR_REASON_KEY = "ERROR_REASON"; + public static final int ERROR_FETCHING_VERSION = 4; + public static final int ERROR_DOWNLOADING_STOPS = 5; + public static final int ERROR_DOWNLOADING_LINES = 6; + + public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; + public static final int SUCCESS_NO_ACTION_NEEDED = 9; + public static final int SUCCESS_UPDATE_DONE = 1; + + private final int notifi_ID=62341; + + public static final String FORCED_UPDATE = "FORCED-UPDATE"; + + public static final String DEBUG_TAG = "Busto-UpdateWorker"; + + + public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @SuppressLint("RestrictedApi") + @NonNull + @Override + public Result doWork() { + //register Notification channel + final Context con = getApplicationContext(); + Notifications.createDefaultNotificationChannel(con); + final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); + final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); + + final int new_DB_version = DatabaseUpdate.getNewVersion(); + + final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); + + final int notificationID = showNotification(); + Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); + Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); + if (new_DB_version < 0){ + //there has been an error + final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) + .putInt(ERROR_CODE_KEY,new_DB_version).build(); + cancelNotification(notificationID); + return Result.failure(out); + } + + //we got a good version + if (current_DB_version >= new_DB_version && !isUpdateCompulsory) { + //don't need to update + cancelNotification(notificationID); + return Result.success(new Data.Builder(). + putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); + } + //start the real update + AtomicReference resultAtomicReference = new AtomicReference<>(); + DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); + final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); + DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); + + if (resultUpdate != DatabaseUpdate.Result.DONE){ + Fetcher.result result = resultAtomicReference.get(); + + final Data.Builder dataBuilder = new Data.Builder(); + switch (resultUpdate){ + case ERROR_STOPS_DOWNLOAD: + dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); + break; + case ERROR_LINES_DOWNLOAD: + dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); + break; + } + cancelNotification(notificationID); + return Result.failure(dataBuilder.build()); + } + Log.d(DEBUG_TAG, "Update finished successfully!"); + //update the version in the shared preference + final SharedPreferences.Editor editor = shPr.edit(); + editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); + editor.apply(); + cancelNotification(notificationID); + + return Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); + } + + public static Constraints getWorkConstraints(){ + return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresCharging(false).build(); + } + + public static WorkRequest newFirstTimeWorkRequest(){ + return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) + .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) + //.setInputData(new Data.Builder().putBoolean()) + .build(); + } + + + private int showNotification(){ + final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), Notifications.DEFAULT_CHANNEL_ID) + .setContentTitle("Libre BusTO - Updating Database") + .setProgress(0,0,true) + .setPriority(NotificationCompat.PRIORITY_LOW); + builder.setSmallIcon(R.drawable.ic_bus_orange); + final NotificationManagerCompat notifcManager = NotificationManagerCompat.from(getApplicationContext()); + final int notification_ID = 32198; + notifcManager.notify(notification_ID,builder.build()); + + return notification_ID; + } + + private void cancelNotification(int notificationID){ + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + + notificationManager.cancel(notificationID); + } +} diff --git a/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java b/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java @@ -0,0 +1,158 @@ +package it.reyboz.bustorino.middleware; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; +import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.Fetcher; +import it.reyboz.bustorino.backend.FiveTAPIFetcher; +import it.reyboz.bustorino.backend.Route; +import it.reyboz.bustorino.backend.Stop; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; + +import static android.content.Context.MODE_PRIVATE; + + +public class DatabaseUpdate { + + public static final String DEBUG_TAG = "BusTO-DBUpdate"; + public static final int VERSION_UNAVAILABLE = -2; + public static final int JSON_PARSING_ERROR = -4; + + public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; + + enum Result { + DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD + } + + /** + * Request the server the version of the database + * @return the version of the DB, or an error code + */ + public static int getNewVersion(){ + AtomicReference gres = new AtomicReference<>(); + String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); + if(networkRequest == null){ + return VERSION_UNAVAILABLE; + } + + try { + JSONObject resp = new JSONObject(networkRequest); + return resp.getInt("id"); + } catch (JSONException e) { + e.printStackTrace(); + Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); + return JSON_PARSING_ERROR; + } + } + /** + * Run the DB Update + * @param con a context + * @param gres a result reference + * @return result of the update + */ + public static Result performDBUpdate(Context con, AtomicReference gres) { + + final FiveTAPIFetcher f = new FiveTAPIFetcher(); + final ArrayList stops = f.getAllStopsFromGTT(gres); + //final ArrayList cpOp = new ArrayList<>(); + + if (gres.get() != Fetcher.result.OK) { + Log.w(DEBUG_TAG, "Something went wrong downloading"); + return Result.ERROR_STOPS_DOWNLOAD; + + } + // return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database + final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext()); + final SQLiteDatabase db = dbHelp.getWritableDatabase(); + //Empty the needed tables + db.beginTransaction(); + //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); + //db.delete(LinesTable.TABLE_NAME,null,null); + + //put new data + long startTime = System.currentTimeMillis(); + + Log.d(DEBUG_TAG, "Inserting " + stops.size() + " stops"); + for (final Stop s : stops) { + final ContentValues cv = new ContentValues(); + + cv.put(NextGenDB.Contract.StopsTable.COL_ID, s.ID); + cv.put(NextGenDB.Contract.StopsTable.COL_NAME, s.getStopDefaultName()); + if (s.location != null) + cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, s.location); + cv.put(NextGenDB.Contract.StopsTable.COL_LAT, s.getLatitude()); + cv.put(NextGenDB.Contract.StopsTable.COL_LONG, s.getLongitude()); + if (s.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName()); + cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString()); + if (s.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, s.type.getCode()); + + //Log.d(DEBUG_TAG,cv.toString()); + //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); + //valuesArr[i] = cv; + db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); + + } + db.setTransactionSuccessful(); + db.endTransaction(); + long endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); + + final ArrayList routes = f.getAllLinesFromGTT(gres); + + if (routes == null) { + Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); + dbHelp.close(); + return Result.ERROR_LINES_DOWNLOAD; + + } + + db.beginTransaction(); + startTime = System.currentTimeMillis(); + for (Route r : routes) { + final ContentValues cv = new ContentValues(); + cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); + switch (r.type) { + case BUS: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); + break; + case RAILWAY: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); + break; + case LONG_DISTANCE_BUS: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); + break; + } + cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); + + //db.insert(LinesTable.TABLE_NAME,null,cv); + int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); + if (rows < 1) { //we haven't changed anything + db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); + dbHelp.close(); + + return Result.DONE; + } + + public static boolean setDBUpdatingFlag(Context con, boolean value){ + final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); + return setDBUpdatingFlag(con, shPr, value); + } + static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ + final SharedPreferences.Editor editor = shPr.edit(); + editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); + return editor.commit(); + } +} diff --git a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java --- a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java +++ b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java @@ -19,24 +19,17 @@ import android.app.IntentService; import android.content.*; -import android.database.sqlite.SQLiteDatabase; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; -import it.reyboz.bustorino.ActivityMain; + import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; -import it.reyboz.bustorino.backend.Route; -import it.reyboz.bustorino.backend.Stop; import org.json.JSONException; import org.json.JSONObject; -import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.concurrent.atomic.AtomicReference; -import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*; - /** * An {@link IntentService} subclass for handling asynchronous task requests in * a service on a separate handler thread. @@ -126,6 +119,10 @@ } private boolean performDBUpdate(AtomicReference gres){ + if(!setDBUpdatingFlag(true)) + return false; + + /* final FiveTAPIFetcher f = new FiveTAPIFetcher(); final ArrayList stops = f.getAllStopsFromGTT(gres); //final ArrayList cpOp = new ArrayList<>(); @@ -212,6 +209,9 @@ Log.d(DEBUG_TAG,"Inserting lines took: "+((double) (endTime-startTime)/1000)+" s"); dbHelp.close(); return true; + + */ + return DatabaseUpdate.performDBUpdate(getApplication(),gres) == DatabaseUpdate.Result.DONE; } private int getNewVersion(UpdateRequestParams params){ AtomicReference gres = new AtomicReference<>(); @@ -252,7 +252,7 @@ } } - /** + /* * Probably useless * * Spoiler: IT IS diff --git a/src/it/reyboz/bustorino/middleware/GeneralActivity.java b/src/it/reyboz/bustorino/middleware/GeneralActivity.java --- a/src/it/reyboz/bustorino/middleware/GeneralActivity.java +++ b/src/it/reyboz/bustorino/middleware/GeneralActivity.java @@ -4,21 +4,42 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; +import com.google.android.material.snackbar.Snackbar; +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; /** * Activity class that contains all the generally useful methods */ public abstract class GeneralActivity extends AppCompatActivity { - final protected int PERMISSION_REQUEST_POSITION = 33; + 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(); @@ -30,6 +51,10 @@ 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) { @@ -43,17 +68,56 @@ NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } - public abstract void showMessage(int messageID); - public abstract void showKeyboard(); + + 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 void assertLocationPermissions() { if(ContextCompat.checkSelfPermission(getApplicationContext(),Manifest.permission.ACCESS_FINE_LOCATION)!=PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_POSITION); } } + + 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) + 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){ diff --git a/src/it/reyboz/bustorino/middleware/NextGenDB.java b/src/it/reyboz/bustorino/middleware/NextGenDB.java --- a/src/it/reyboz/bustorino/middleware/NextGenDB.java +++ b/src/it/reyboz/bustorino/middleware/NextGenDB.java @@ -19,21 +19,24 @@ import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; -import it.reyboz.bustorino.R; + +import it.reyboz.bustorino.backend.Fetcher; +import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; +import org.json.JSONException; +import org.json.JSONObject; import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*; @@ -95,22 +98,6 @@ appContext = context.getApplicationContext(); } - /** - * Lazy initialization singleton getter, thread-safe with double checked locking - * from https://en.wikipedia.org/wiki/Singleton_pattern - * @return the instance - */ - /* - public static NextGenDB getInstance(Context context){ - if(instance==null){ - synchronized (NextGenDB.class){ - if(instance==null){ - instance = new NextGenDB(context); - } - } - } - return instance; - }*/ @Override public void onCreate(SQLiteDatabase db) { @@ -364,5 +351,4 @@ super(message); } } - } diff --git a/src/it/reyboz/bustorino/middleware/StopsDB.java b/src/it/reyboz/bustorino/middleware/StopsDB.java --- a/src/it/reyboz/bustorino/middleware/StopsDB.java +++ b/src/it/reyboz/bustorino/middleware/StopsDB.java @@ -22,8 +22,8 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.readystatesoftware.sqliteasset.SQLiteAssetHelper; diff --git a/src/it/reyboz/bustorino/util/RoutePositionSorter.java b/src/it/reyboz/bustorino/util/RoutePositionSorter.java --- a/src/it/reyboz/bustorino/util/RoutePositionSorter.java +++ b/src/it/reyboz/bustorino/util/RoutePositionSorter.java @@ -18,9 +18,9 @@ package it.reyboz.bustorino.util; import android.location.Location; -import android.support.v4.util.Pair; +import androidx.core.util.Pair; import android.util.Log; -import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; + import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop;