res) {
SystemClock.sleep(5000);
res.set(result.SERVER_ERROR);
return new Palina();
}
}
private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/
private ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()};
private StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()};
/*
* Position
*/
//Fine location criteria
private final Criteria cr = new Criteria();
private boolean pendingNearbyStopsRequest = false;
private LocationManager locmgr;
private FragmentHelper fh;
///////////////////////////////// EVENT HANDLERS ///////////////////////////////////////////////
/*
* @see swipeRefreshLayout
*/
private final Handler theHandler = new Handler();
private final Runnable refreshStop = new Runnable() {
public void run() {
if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame);
if (fragment == null){
new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute();
} else{
String stopName = fragment.getStopID();
new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray(), getApplicationContext()).execute(stopName);
}
} else //we create a new fragment, which is WRONG
new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute();
}
};
//// MAIN METHOD ///
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
framan = getSupportFragmentManager();
final SharedPreferences theShPr = getMainSharedPreferences();
/*
* UI
*/
setContentView(R.layout.activity_main);
Toolbar defToolbar = findViewById(R.id.that_toolbar);
setSupportActionBar(defToolbar);
busStopSearchByIDEditText = findViewById(R.id.busStopSearchByIDEditText);
busStopSearchByNameEditText = findViewById(R.id.busStopSearchByNameEditText);
progressBar = findViewById(R.id.progressBar);
howDoesItWorkTextView = findViewById(R.id.howDoesItWorkTextView);
hideHintButton = findViewById(R.id.hideHintButton);
swipeRefreshLayout = findViewById(R.id.listRefreshLayout);
floatingActionButton = findViewById(R.id.floatingActionButton);
framan.addOnBackStackChangedListener(() -> Log.d("MainActivity, BusTO", "BACK STACK CHANGED"));
busStopSearchByIDEditText.setSelectAllOnFocus(true);
busStopSearchByIDEditText
.setOnEditorActionListener((v, actionId, event) -> {
// IME_ACTION_SEARCH alphabetical option
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
onSearchClick(v);
return true;
}
return false;
});
busStopSearchByNameEditText
.setOnEditorActionListener((v, actionId, event) -> {
// IME_ACTION_SEARCH alphabetical option
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
onSearchClick(v);
return true;
}
return false;
});
// Called when the layout is pulled down
swipeRefreshLayout
.setOnRefreshListener(() -> theHandler.post(refreshStop));
/**
* @author Marco Gagino!!!
*/
//swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't
swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500);
fh = new FragmentHelper(this, framan, getApplicationContext(),R.id.resultFrame);
setSearchModeBusStopID();
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
String busStopDisplayName = null;
Uri data = getIntent().getData();
if (data != null) {
busStopID = getBusStopIDFromUri(data);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
busStopDisplayName = b.getString("bus-stop-display-name");
/**
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
setBusStopSearchByIDEditText(busStopID);
/*
//THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS
// STOP IS ADDED AUTOMATICALLY
Stop nextStop = new Stop(busStopID);
// forcing it as user name even though it could be standard name, it doesn't really matter
nextStop.setStopUserName(busStopDisplayName);
//set stop as last succe
fh.setLastSuccessfullySearchedBusStop(nextStop);
*/
requestArrivalsForStopID(busStopID);
}
//Try (hopefully) database update
PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
final WorkManager workManager = WorkManager.getInstance(this);
final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10);
if (version >= 0)
workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.KEEP, wr);
else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.REPLACE, wr);
/*
Set database update
*/
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
.observe(this, workInfoList -> {
// If there are no matching work info, do nothing
if (workInfoList == null || workInfoList.isEmpty()) {
return;
}
Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList);
boolean showProgress = false;
for (WorkInfo workInfo : workInfoList) {
if (workInfo.getState() == WorkInfo.State.RUNNING) {
showProgress = true;
}
}
if (showProgress) {
createDefaultSnackbar();
} else {
if(snackbar!=null) {
snackbar.dismiss();
snackbar = null;
}
}
});
//locationHandler = new GPSLocationAdapter(getApplicationContext());
//--------- NEARBY STOPS--------//
//SETUP LOCATION
locmgr = (LocationManager) getSystemService(LOCATION_SERVICE);
cr.setAccuracy(Criteria.ACCURACY_FINE);
cr.setAltitudeRequired(false);
cr.setBearingRequired(false);
cr.setCostAllowed(true);
cr.setPowerRequirement(Criteria.NO_REQUIREMENT);
//We want the nearby bus stops!
theHandler.post(new NearbyStopsRequester(this));
//If there are no providers available, then, wait for them
Log.d("MainActivity", "Created");
}
/*
* Reload bus stop timetable when it's fulled resumed from background.
* @Override protected void onPostResume() {
* super.onPostResume();
* Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop());
* if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) {
* setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID);
* new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute();
* } else {
* //we have new activity or we don't have a new searched stop.
* //Let's search stops nearby
* LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE);
* Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.resultFrame);
*
*
* }
* //show the FAB since it remains hidden
* floatingActionButton.show();
*
* }
**/
@Override
protected void onPause() {
super.onPause();
fh.stopLastRequestIfNeeded();
fh.setBlockAllActivities(true);
locmgr.removeUpdates(locListener);
}
@Override
protected void onResume() {
super.onResume();
fh.setBlockAllActivities(false);
//TODO: check if current LiveData-bound observer works
if (pendingNearbyStopsRequest)
theHandler.post(new NearbyStopsRequester(this));
ActionBar bar = getSupportActionBar();
if(bar!=null) bar.show();
else Log.w(DEBUG_TAG, "ACTION BAR IS NULL");
//check if we can display the experiments or not
SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
//Log.w(DEBUG_TAG, "Preference experimental is "+exper_On);
//MenuItem experimentsItem =
if (experimentsMenuItem != null)
experimentsMenuItem.setVisible(exper_On);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
actionHelpMenuItem = menu.findItem(R.id.action_help);
experimentsMenuItem = menu.findItem(R.id.action_experiments);
SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
experimentsMenuItem.setVisible(exper_On);
return true;
}
/**
* Callback fired when a MenuItem is selected
*
* @param item
* @return
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
Resources res = getResources();
switch (item.getItemId()) {
case android.R.id.home:
// Respond to the action bar's Up/Home button
NavUtils.navigateUpFromSameTask(this);
return true;
case R.id.action_help:
showHints();
return true;
case R.id.action_favorites:
startActivity(new Intent(ActivityMain.this, ActivityFavorites.class));
return true;
case R.id.action_map:
//ensure storage permission is granted
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
switch (result) {
case PERMISSION_OK:
startActivity(new Intent(ActivityMain.this, ActivityMap.class));
break;
case PERMISSION_ASKING:
permissionDoneRunnables.put(permission,
() -> startActivity(new Intent(ActivityMain.this, ActivityMap.class)));
break;
case PERMISSION_NEG_CANNOT_ASK:
String storage_perm = res.getString(R.string.storage_permission);
String text = res.getString(R.string.too_many_permission_asks, storage_perm);
Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show();
}
return true;
case R.id.action_about:
startActivity(new Intent(ActivityMain.this, ActivityAbout.class));
return true;
case R.id.action_hack:
openIceweasel(res.getString(R.string.hack_url));
return true;
case R.id.action_source:
openIceweasel("https://gitpull.it/source/libre-busto/");
return true;
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html");
return true;
case R.id.action_donate:
openIceweasel("https://www.liberapay.com/Libre_BusTO/");
return true;
case R.id.action_settings:
Log.d("MAINBusTO", "Pressed button preferences");
startActivity(new Intent(ActivityMain.this, ActivitySettings.class));
return true;
case R.id.action_experiments:
startActivity(new Intent(this, ActivityPrincipal.class));
}
return super.onOptionsItemSelected(item);
}
/**
* OK this is pure shit
*
* @param v View clicked
*/
public void onSearchClick(View v) {
if (searchMode == SEARCH_BY_ID) {
String busStopID = busStopSearchByIDEditText.getText().toString();
requestArrivalsForStopID(busStopID);
} else { // searchMode == SEARCH_BY_NAME
String query = busStopSearchByNameEditText.getText().toString();
//new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper);
new AsyncDataDownload(fh, stopsFinderByNames, getApplicationContext()).execute(query);
}
}
/**
* PERMISSION STUFF
**/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSION_REQUEST_POSITION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setOption(LOCATION_PERMISSION_GIVEN, true);
//if we sent a request for a new NearbyStopsFragment
if (pendingNearbyStopsRequest) {
pendingNearbyStopsRequest = false;
theHandler.post(new NearbyStopsRequester(this));
}
} else {
//permission denied
setOption(LOCATION_PERMISSION_GIVEN, false);
}
//add other cases for permissions
break;
case STORAGE_PERMISSION_REQ:
final String storageKey = Manifest.permission.WRITE_EXTERNAL_STORAGE;
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions));
if (permissionDoneRunnables.containsKey(storageKey)) {
Runnable toRun = permissionDoneRunnables.get(storageKey);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storageKey);
}
} else {
//permission denied
showToastMessage(R.string.permission_storage_maps_msg, false);
/*final int canGetPermission = askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, STORAGE_PERMISSION_REQ);
switch (canGetPermission) {
case PERMISSION_ASKING:
break;
case PERMISSION_NEG_CANNOT_ASK:
permissionDoneRunnables.remove(storageKey);
showToastMessage(R.string.closing_act_crash_msg, false);
}*/
}
}
}
@Override
public void requestArrivalsForStopID(String ID) {
if (ID == null || ID.length() <= 0) {
// we're still in UI thread, no need to mess with Progress
showToastMessage(R.string.insert_bus_stop_number_error, true);
toggleSpinner(false);
} else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame);
if (fragment !=null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){
// Run with previous fetchers
//fragment.getCurrentFetchers().toArray()
new AsyncDataDownload(fh,fragment.getCurrentFetchersAsArray(), this).execute(ID);
} else{
new AsyncDataDownload(fh, arrivalsFetchers, this).execute(ID);
}
}
else {
new AsyncDataDownload(fh, arrivalsFetchers, this).execute(ID);
Log.d("MainActiv", "Started search for arrivals of stop " + ID);
}
}
/**
* QR scan button clicked
*
* @param v View QRButton clicked
*/
public void onQRButtonClick(View v) {
IntentIntegrator integrator = new IntentIntegrator(this);
integrator.initiateScan();
}
/**
* Receive the Barcode Scanner Intent
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
Uri uri;
try {
uri = Uri.parse(scanResult != null ? scanResult.getContents() : null); // this apparently prevents NullPointerException. Somehow.
} catch (NullPointerException e) {
Toast.makeText(getApplicationContext(),
R.string.no_qrcode, Toast.LENGTH_SHORT).show();
return;
}
String busStopID = getBusStopIDFromUri(uri);
busStopSearchByIDEditText.setText(busStopID);
requestArrivalsForStopID(busStopID);
}
public void onHideHint(View v) {
hideHints();
setOption(OPTION_SHOW_LEGEND, false);
}
public void onToggleKeyboardLayout(View v) {
if (searchMode == SEARCH_BY_NAME) {
setSearchModeBusStopID();
if (busStopSearchByIDEditText.requestFocus()) {
showKeyboard();
}
} else { // searchMode == SEARCH_BY_ID
setSearchModeBusStopName();
if (busStopSearchByNameEditText.requestFocus()) {
showKeyboard();
}
}
}
private void createDefaultSnackbar() {
if (snackbar == null) {
snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE);
}
snackbar.show();
}
///////////////////////////////// POSITION STUFF//////////////////////////////////////////////
private void resolveStopRequest(String provider) {
Log.d(DEBUG_TAG, "Provider " + provider + " got enabled");
if (locmgr != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) {
pendingNearbyStopsRequest = false;
theHandler.post(new NearbyStopsRequester(this));
}
}
final LocationListener locListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
Log.d(DEBUG_TAG, "Location changed");
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d(DEBUG_TAG, "Location provider status: " + status);
if (status == LocationProvider.AVAILABLE) {
resolveStopRequest(provider);
}
}
@Override
public void onProviderEnabled(String provider) {
resolveStopRequest(provider);
}
@Override
public void onProviderDisabled(String provider) {
}
};
/**
* Run location requests separately and asynchronously
*/
class NearbyStopsRequester implements Runnable {
Activity runningAct;
public NearbyStopsRequester(Activity runningAct) {
this.runningAct = runningAct;
}
@Override
public void run() {
final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false);
final boolean noPermission = ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED;
//if we don't have the permission, we have to ask for it, if we haven't
// asked too many times before
if (noPermission) {
if (!canRunPosition) {
pendingNearbyStopsRequest = true;
Permissions.assertLocationPermissions(getApplicationContext(),runningAct);
Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission);
return;
} else {
Toast.makeText(getApplicationContext(), "Asked for permission position too many times", Toast.LENGTH_LONG).show();
}
} else setOption(LOCATION_PERMISSION_GIVEN, true);
LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE);
if (locManager == null) {
Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment");
return;
}
if (anyLocationProviderMatchesCriteria(locManager, cr, true)
&& fh.getLastSuccessfullySearchedBusStop() == null
&& !framan.isDestroyed()) {
//Go ahead with the request
Log.d("mainActivity", "Recreating stop fragment");
swipeRefreshLayout.setVisibility(View.VISIBLE);
NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS);
Fragment oldFrag = framan.findFragmentById(R.id.resultFrame);
FragmentTransaction ft = framan.beginTransaction();
if (oldFrag != null)
ft.remove(oldFrag);
ft.add(R.id.resultFrame, fragment, "nearbyStop_correct");
ft.commit();
framan.executePendingTransactions();
pendingNearbyStopsRequest = false;
} else if (!anyLocationProviderMatchesCriteria(locManager, cr, true)) {
//Wait for the providers
Log.d(DEBUG_TAG, "Queuing position request");
pendingNearbyStopsRequest = true;
locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, locListener);
}
}
}
private boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) {
List providers = mng.getProviders(cr, enabled);
Log.d(DEBUG_TAG, "Getting enabled location providers: ");
for (String s : providers) {
Log.d(DEBUG_TAG, "Provider " + s);
}
return providers.size() > 0;
}
///////////////////////////////// OTHER STUFF //////////////////////////////////////////////////
@Override
public void showFloatingActionButton(boolean yes) {
if (yes) floatingActionButton.show();
else floatingActionButton.hide();
}
@Override
public void enableRefreshLayout(boolean yes) {
swipeRefreshLayout.setEnabled(yes);
}
////////////////////////////////////// GUI HELPERS /////////////////////////////////////////////
public void showKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText;
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
private void setSearchModeBusStopID() {
searchMode = SEARCH_BY_ID;
busStopSearchByNameEditText.setVisibility(View.GONE);
busStopSearchByNameEditText.setText("");
busStopSearchByIDEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.alphabetical);
}
private void setSearchModeBusStopName() {
searchMode = SEARCH_BY_NAME;
busStopSearchByIDEditText.setVisibility(View.GONE);
busStopSearchByIDEditText.setText("");
busStopSearchByNameEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.numeric);
}
/**
* Having that cursor at the left of the edit text makes me cancer.
*
* @param busStopID bus stop ID
*/
private void setBusStopSearchByIDEditText(String busStopID) {
busStopSearchByIDEditText.setText(busStopID);
busStopSearchByIDEditText.setSelection(busStopID.length());
}
private void showHints() {
howDoesItWorkTextView.setVisibility(View.VISIBLE);
hideHintButton.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(false);
}
private void hideHints() {
howDoesItWorkTextView.setVisibility(View.GONE);
hideHintButton.setVisibility(View.GONE);
actionHelpMenuItem.setVisible(true);
}
//TODO: toggle spinner from mainActivity
@Override
public void toggleSpinner(boolean enable) {
if (enable) {
//already set by the RefreshListener when needed
//swipeRefreshLayout.setRefreshing(true);
progressBar.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
}
}
private void prepareGUIForBusLines() {
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(true);
}
private void prepareGUIForBusStops() {
swipeRefreshLayout.setEnabled(false);
swipeRefreshLayout.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(false);
}
/**
* This provides a temporary fix to make the transition
* to a single asynctask go smoother
*
* @param fragmentType the type of fragment created
*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
hideKeyboard();
//if we are getting results, already, stop waiting for nearbyStops
if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) {
locmgr.removeUpdates(locListener);
pendingNearbyStopsRequest = false;
}
if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType");
else
switch (fragmentType) {
case ARRIVALS:
prepareGUIForBusLines();
if (getOption(OPTION_SHOW_LEGEND, true)) {
showHints();
}
break;
case STOPS:
prepareGUIForBusStops();
break;
default:
Log.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment");
return;
}
// Shows hints
}
private void openIceweasel(String url){
utils.openIceweasel(url, this);
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java
index a162c92..fe85fe5 100644
--- a/src/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/src/it/reyboz/bustorino/ActivityPrincipal.java
@@ -1,373 +1,433 @@
package it.reyboz.bustorino;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.util.concurrent.TimeUnit;
import it.reyboz.bustorino.data.DBUpdateWorker;
import it.reyboz.bustorino.data.DatabaseUpdate;
+import it.reyboz.bustorino.fragments.FavoritesFragment;
import it.reyboz.bustorino.fragments.FragmentKind;
import it.reyboz.bustorino.fragments.FragmentListenerMain;
import it.reyboz.bustorino.fragments.MainScreenFragment;
import it.reyboz.bustorino.middleware.GeneralActivity;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.backend.utils.openIceweasel;
public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain {
private DrawerLayout mDrawer;
private NavigationView mNavView;
private ActionBarDrawerToggle drawerToggle;
private final static String DEBUG_TAG="BusTO Act Principal";
+ private final static String TAG_FAVORITES="favorites_frag";
private Snackbar snackbar;
+ private boolean showingMainFragmentFromOther = false;
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
final SharedPreferences theShPr = getMainSharedPreferences();
Toolbar mToolbar = findViewById(R.id.default_toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar()!=null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
else Log.w(DEBUG_TAG, "NO ACTION BAR");
mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener());
mDrawer = findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle(mToolbar);
// Setup toggle to display hamburger icon with nice animation
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
mDrawer.addDrawerListener(drawerToggle);
mNavView = findViewById(R.id.nvView);
setupDrawerContent(mNavView);
/// LEGACY CODE
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
Uri data = getIntent().getData();
if (data != null) {
busStopID = getBusStopIDFromUri(data);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
/**
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
//setBusStopSearchByIDEditText(busStopID);
requestArrivalsForStopID(busStopID);
}
//Try (hopefully) database update
PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
final WorkManager workManager = WorkManager.getInstance(this);
final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10);
if (version >= 0)
workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.KEEP, wr);
else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
ExistingPeriodicWorkPolicy.REPLACE, wr);
/*
Set database update
*/
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
.observe(this, workInfoList -> {
// If there are no matching work info, do nothing
if (workInfoList == null || workInfoList.isEmpty()) {
return;
}
Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList);
boolean showProgress = false;
for (WorkInfo workInfo : workInfoList) {
if (workInfo.getState() == WorkInfo.State.RUNNING) {
showProgress = true;
}
}
if (showProgress) {
createDefaultSnackbar();
} else {
if(snackbar!=null) {
snackbar.dismiss();
snackbar = null;
}
}
});
// show the main fragment
showMainFragment();
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
// and will not render the hamburger icon without it.
return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
menuItem -> {
if (menuItem.getItemId() == R.id.drawer_action_settings) {
Log.d("MAINBusTO", "Pressed button preferences");
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class));
return true;
+ } else if(menuItem.getItemId() == R.id.nav_favorites_item){
+ closeDrawerIfOpen();
+ //get Fragment
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ FavoritesFragment fragment = FavoritesFragment.newInstance();
+ ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES);
+ ft.addToBackStack(null);
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ ft.commit();
+ return true;
+ } else if(menuItem.getItemId() == R.id.nav_arrivals){
+ closeDrawerIfOpen();
+ showMainFragment();
+ return true;
}
//selectDrawerItem(menuItem);
Log.d(DEBUG_TAG, "pressed item "+menuItem.toString());
return true;
});
}
private void closeDrawerIfOpen(){
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
}
// `onPostCreate` called when activity start-up is complete after `onStart()`
// NOTE 1: Make sure to override the method with only a single `Bundle` argument
// Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method.
// There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon.
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.extra_menu_items, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item};
Log.d(DEBUG_TAG, "Item pressed");
- switch (item.getItemId()){
- case android.R.id.home:
- mDrawer.openDrawer(GravityCompat.START);
- return true;
- default:
-
+ if (item.getItemId() == android.R.id.home) {
+ mDrawer.openDrawer(GravityCompat.START);
+ return true;
}
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
boolean foundFragment = false;
Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){
- shownFrag.getChildFragmentManager().popBackStack();
+ //if we have been asked to show a stop from another fragment, we should go back even in the main
+ shownFrag.getChildFragmentManager().popBackStackImmediate();
+ if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){
+ getSupportFragmentManager().popBackStack();
+ }
}
else if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
}
else
super.onBackPressed();
}
private void createDefaultSnackbar() {
if (snackbar == null) {
snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE);
}
snackbar.show();
}
- private MainScreenFragment showMainFragment(){
+ private MainScreenFragment createAndShowMainFragment(){
FragmentManager fraMan = getSupportFragmentManager();
MainScreenFragment fragment = MainScreenFragment.newInstance();
FragmentTransaction transaction = fraMan.beginTransaction();
transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG);
transaction.commit();
return fragment;
}
+
+ /**
+ * Show the fragment by adding it to the backstack
+ * @param fraMan the fragmentManager
+ * @param fragment the fragment
+ */
+ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment){
+ fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment)
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ /*.setCustomAnimations(
+ R.anim.slide_in, // enter
+ R.anim.fade_out, // exit
+ R.anim.fade_in, // popEnter
+ R.anim.slide_out // popExit
+ )*/
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ .commit();
+ }
+
+ private MainScreenFragment showMainFragment(){
+ FragmentManager fraMan = getSupportFragmentManager();
+ Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
+ MainScreenFragment mainScreenFragment = null;
+ if (fragment==null | !(fragment instanceof MainScreenFragment)){
+ mainScreenFragment = createAndShowMainFragment();
+ }
+ else if(!fragment.isVisible()){
+
+
+ mainScreenFragment = (MainScreenFragment) fragment;
+ showMainFragment(fraMan, mainScreenFragment);
+ Log.d(DEBUG_TAG, "Found the main fragment");
+ } else{
+ mainScreenFragment = (MainScreenFragment) fragment;
+ }
+ return mainScreenFragment;
+ }
@Nullable
private MainScreenFragment getMainFragmentIfVisible(){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment;
else return null;
}
+
@Override
public void showFloatingActionButton(boolean yes) {
//TODO
}
@Override
public void readyGUIfor(FragmentKind fragmentType) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.readyGUIfor(fragmentType);
}
}
@Override
public void requestArrivalsForStopID(String ID) {
- FragmentManager fraMan = getSupportFragmentManager();
- Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
- MainScreenFragment mainScreenFragment = null;
- if (fragment==null | !(fragment instanceof MainScreenFragment)){
- mainScreenFragment = showMainFragment();
- }
- else if(!fragment.isVisible()){
-
- fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment)
- .addToBackStack(null)
- .commit();
- mainScreenFragment = (MainScreenFragment) fragment;
- Log.d(DEBUG_TAG, "Found the main fragment");
- } else{
- mainScreenFragment = (MainScreenFragment) fragment;
+ //register if the request came from the main fragment or not
+ MainScreenFragment probableFragment = getMainFragmentIfVisible();
+ showingMainFragmentFromOther = (probableFragment==null);
+
+ if (showingMainFragmentFromOther){
+ FragmentManager fraMan = getSupportFragmentManager();
+ Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
+ if(fragment!=null){
+ //the fragment is there but not shown
+ probableFragment = (MainScreenFragment) fragment;
+ // set the flag
+ probableFragment.setSuppressArrivalsReload(true);
+ showMainFragment(fraMan, probableFragment);
+ } else {
+ // we have no fragment
+ probableFragment = createAndShowMainFragment();
+ }
}
-
- mainScreenFragment.requestArrivalsForStopID(ID);
+ probableFragment.requestArrivalsForStopID(ID);
+ mNavView.setCheckedItem(R.id.nav_arrivals);
}
@Override
public void toggleSpinner(boolean state) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.toggleSpinner(state);
}
}
@Override
public void enableRefreshLayout(boolean yes) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.enableRefreshLayout(yes);
}
}
class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_about:
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
case R.id.action_hack:
openIceweasel(getString(R.string.hack_url), getApplicationContext());
return true;
case R.id.action_source:
openIceweasel("https://gitpull.it/source/libre-busto/", getApplicationContext());
return true;
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", getApplicationContext());
return true;
default:
}
return false;
}
}
}
diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java
index 6ece6cc..da97419 100644
--- a/src/it/reyboz/bustorino/backend/Stop.java
+++ b/src/it/reyboz/bustorino/backend/Stop.java
@@ -1,295 +1,295 @@
/*
BusTO (backend components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import it.reyboz.bustorino.util.LinesNameSorter;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class Stop implements Comparable {
// remove "final" in case you need to set these from outside the parser\scrapers\fetchers
public final @NonNull String ID;
private @Nullable String name;
private @Nullable String username;
public @Nullable String location;
public @Nullable Route.Type type;
private @Nullable List routesThatStopHere;
private final @Nullable Double lat;
private final @Nullable Double lon;
// leave this non-final
private @Nullable String routesThatStopHereString = null;
private @Nullable String absurdGTTPlaceName = null;
/**
* Hey, look, method overloading!
*/
public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) {
this.ID = ID;
this.name = name;
this.username = null;
this.location = (location != null && location.length() == 0) ? null : location;
this.type = type;
this.routesThatStopHere = routesThatStopHere;
this.lat = null;
this.lon = null;
}
/**
* Hey, look, method overloading!
*/
public Stop(final @NonNull String ID) {
this.ID = ID;
this.name = null;
this.username = null;
this.location = null;
this.type = null;
this.routesThatStopHere = null;
this.lat = null;
this.lon = null;
}
/**
* Constructor that sets EVERYTHING.
*/
public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon) {
this.ID = ID;
this.name = name;
this.username = userName;
this.location = location;
this.type = type;
this.routesThatStopHere = routesThatStopHere;
this.lat = lat;
this.lon = lon;
}
public @Nullable String routesThatStopHereToString() {
// M E M O I Z A T I O N
if(this.routesThatStopHereString != null) {
return this.routesThatStopHereString;
}
// no string yet? build it!
- return buildString();
+ return buildRoutesString();
}
@Nullable
public String getAbsurdGTTPlaceName() {
return absurdGTTPlaceName;
}
public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) {
this.absurdGTTPlaceName = absurdGTTPlaceName;
}
public void setRoutesThatStopHere(@Nullable List routesThatStopHere) {
this.routesThatStopHere = routesThatStopHere;
}
@Nullable
protected List getRoutesThatStopHere(){
return routesThatStopHere;
}
- private @Nullable String buildString() {
+ private @Nullable String buildRoutesString() {
// no routes => no string
if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) {
return null;
}
StringBuilder sb = new StringBuilder();
Collections.sort(routesThatStopHere,new LinesNameSorter());
int i, lenMinusOne = routesThatStopHere.size() - 1;
for (i = 0; i < lenMinusOne; i++) {
sb.append(routesThatStopHere.get(i)).append(", ");
}
// last one:
sb.append(routesThatStopHere.get(i));
this.routesThatStopHereString = sb.toString();
return this.routesThatStopHereString;
}
@Override
public int compareTo(@NonNull Stop other) {
int res;
int thisAsInt = networkTools.failsafeParseInt(this.ID);
int otherAsInt = networkTools.failsafeParseInt(other.ID);
// numeric stop IDs
if(thisAsInt != 0 && otherAsInt != 0) {
return thisAsInt - otherAsInt;
} else {
// non-numeric
res = this.ID.compareTo(other.ID);
if (res != 0) {
return res;
}
}
// try with name, then
if(this.name != null && other.name != null) {
res = this.name.compareTo(other.name);
}
// and give up
return res;
}
/**
* Sets a name.
*
* @param name stop name as string (not null)
*/
public final void setStopName(@NonNull String name) {
this.name = name;
}
/**
* Sets user name. Empty string is converted to null.
*
* @param name a string of non-zero length, or null
*/
public final void setStopUserName(@Nullable String name) {
if(name == null) {
this.username = null;
} else if(name.length() == 0) {
this.username = null;
} else {
this.username = name;
}
}
/**
* Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
*
* @return string if known, null if still unknown
*/
public final @Nullable String getStopDisplayName() {
if(this.username == null) {
return this.name;
} else {
return this.username;
}
}
/**
* Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName
* refers to the method exactly above this one and not some arcane and esoteric unknown symbol.
*/
public final @Nullable String getStopDefaultName() {
return this.name;
}
/**
* Same as getStopDisplayName, only returns user name.
* Also, never an empty string.
*/
public final @Nullable String getStopUserName() {
return this.username;
}
/**
* Gets username and name from other stop if they exist, sets itself accordingly.
*
* @param other another Stop
* @return did we actually set/change anything?
*/
public final boolean mergeNameFrom(Stop other) {
boolean ret = false;
if(other.name != null) {
if(this.name == null || !this.name.equals(other.name)) {
this.name = other.name;
ret = true;
}
}
if(other.username != null) {
if(this.username == null || !this.username.equals(other.username)) {
this.username = other.username;
ret = true;
}
}
return ret;
}
public final @Nullable String getGeoURL() {
if(this.lat == null || this.lon == null) {
return null;
}
// Android documentation suggests US for machine readable output (use dot as decimal separator)
return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon);
}
public final @Nullable String getGeoURLWithAddress() {
String url = getGeoURL();
if(url == null) {
return null;
}
if(this.location != null) {
try {
String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8"));
return url.concat(addThis);
} catch (Exception ignored) {}
}
return url;
}
@Nullable
public Double getLatitude() {
return lat;
}
@Nullable
public Double getLongitude() {
return lon;
}
public Double getDistanceFromLocation(Location loc){
if(this.lat!=null && this.lon !=null)
return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude());
else return Double.POSITIVE_INFINITY;
}
}
diff --git a/src/it/reyboz/bustorino/backend/StopsDBInterface.java b/src/it/reyboz/bustorino/backend/StopsDBInterface.java
index 41fc5c7..9520987 100644
--- a/src/it/reyboz/bustorino/backend/StopsDBInterface.java
+++ b/src/it/reyboz/bustorino/backend/StopsDBInterface.java
@@ -1,66 +1,57 @@
/*
BusTO (backend components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* No reference to SQLite whatsoever, here.
* Don't get StopsDB inside the backend, use this interface instead.
*/
public interface StopsDBInterface {
/**
* Given a stop ID, get which routes stop there (as strings, there's no sane way to determine their destination\terminus from the database)
*
* @param stopID stop ID
* @return list of routes or null if none (or database closed)
*/
@Nullable List getRoutesByStop(@NonNull String stopID);
- /**
- * Stop ID goes in, stop name comes out.
- * GTT API doesn't return this useful piece of information, so here we go, get it from the database!
- *
- * @param stopID stop ID, in normalized form
- * @return stop name or null if not found (or database closed)
- */
- @Nullable String getNameFromID(@NonNull String stopID);
-
/**
* Stop ID goes in, stop location comes out.
* This is sometimes missing in GTT API, but database contains meaningful locations for nearly every stop...
*
* @param stopID stop ID, in normalized form
* @return stop location or null if not found (or database closed)
*/
@Nullable String getLocationFromID(@NonNull String stopID);
/**
* SELECT * FROM ...
* (No, it doesn't really use *)
* Doesn't set user name, since it's not a default information, but stil...
*
* @param stopID stop ID
* @return Stop with every available piece of data set or null if not found (or database closed)
*/
@Nullable Stop getAllFromID(@NonNull String stopID);
}
diff --git a/src/it/reyboz/bustorino/data/AppDataProvider.java b/src/it/reyboz/bustorino/data/AppDataProvider.java
index 790edb3..99e7322 100644
--- a/src/it/reyboz/bustorino/data/AppDataProvider.java
+++ b/src/it/reyboz/bustorino/data/AppDataProvider.java
@@ -1,269 +1,283 @@
/*
BusTO (middleware)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.*;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
import it.reyboz.bustorino.BuildConfig;
import it.reyboz.bustorino.backend.DBStatusManager;
+import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.NextGenDB.Contract.*;
import java.util.List;
+import static it.reyboz.bustorino.data.UserDB.getFavoritesColumnNamesAsArray;
+
public class AppDataProvider extends ContentProvider {
public static final String AUTHORITY = BuildConfig.APPLICATION_ID +".provider";
private static final int STOP_OP = 1;
private static final int LINE_OP = 2;
private static final int BRANCH_OP = 3;
private static final int FAVORITES_OP =4;
private static final int MANY_STOPS = 5;
private static final int ADD_UPDATE_BRANCHES = 6;
private static final int LINE_INSERT_OP = 7;
private static final int CONNECTIONS = 8;
private static final int LOCATION_SEARCH = 9;
+ private static final int GET_ALL_FAVORITES =10;
+
+ public static final String FAVORITES = "favorites";
private static final String DEBUG_TAG="AppDataProvider";
private Context con;
private NextGenDB appDBHelper;
private UserDB userDBHelper;
private SQLiteDatabase db;
private DBStatusManager preferences;
public AppDataProvider() {
}
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize.
*/
sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP);
sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS);
sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP);
sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP);
sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP);
sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES);
sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS);
sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP);
+ sUriMatcher.addURI(AUTHORITY,FAVORITES,GET_ALL_FAVORITES);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
db = appDBHelper.getWritableDatabase();
int rows;
switch (sUriMatcher.match(uri)){
case MANY_STOPS:
rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null);
break;
default:
throw new UnsupportedOperationException("Not yet implemented");
}
return rows;
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
int match = sUriMatcher.match(uri);
String baseTypedir = "vnd.android.cursor.dir/";
String baseTypeitem = "vnd.android.cursor.item/";
switch (match){
case LOCATION_SEARCH:
return baseTypedir+"stop";
case LINE_OP:
return baseTypeitem+"line";
case CONNECTIONS:
return baseTypedir+"stops";
}
return baseTypedir+"/item";
}
@Override
public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{
//AVOID OPENING A DB CONNECTION, WILL THROW VERY NASTY ERRORS
if(preferences.isDBUpdating(true))
return null;
db = appDBHelper.getWritableDatabase();
Uri finalUri;
long last_rowid = -1;
switch (sUriMatcher.match(uri)){
case ADD_UPDATE_BRANCHES:
Log.d("InsBranchWithProvider","new Insert request");
String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME);
if(line_name==null) throw new IllegalArgumentException("No line name given");
long lineid = -1;
Cursor c = db.query(LinesTable.TABLE_NAME,
new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?",
new String[]{line_name},null,null,null);
Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches");
if(c.getCount() == 0){
//There are no lines, insert?
//NOPE
/*
c.close();
ContentValues cv = new ContentValues();
cv.put(LinesTable.COLUMN_NAME,line_name);
lineid = db.insert(LinesTable.TABLE_NAME,null,cv);
*/
break;
}else {
c.moveToFirst();
/*
while(c.moveToNext()){
Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n"
+c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION)));
}*/
lineid = c.getInt(c.getColumnIndex(NextGenDB.Contract.LinesTable._ID));
c.close();
}
values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME);
values.put(BranchesTable.COL_LINE,lineid);
last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE);
break;
case MANY_STOPS:
//Log.d("AppDataProvider_busTO","New stop insert request");
try{
last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values);
} catch (SQLiteConstraintException e){
Log.w("AppDataProvider_busTO","Insert failed because of constraint");
last_rowid = -1;
e.printStackTrace();
}
break;
case CONNECTIONS:
try{
last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values);
} catch (SQLiteConstraintException e){
Log.w("AppDataProvider_busTO","Insert failed because of constraint");
last_rowid = -1;
e.printStackTrace();
}
break;
default:
throw new IllegalArgumentException("Invalid parameters");
}
finalUri = ContentUris.withAppendedId(uri,last_rowid);
return finalUri;
}
@Override
public boolean onCreate() {
con = getContext();
appDBHelper = new NextGenDB(getContext());
userDBHelper = new UserDB(getContext());
if(con!=null) {
preferences = new DBStatusManager(con,null);
} else {
preferences = null;
Log.e(DEBUG_TAG,"Cannot get shared preferences");
}
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException {
//IMPORTANT
//The app should not query when the DB is updating, but apparently, it does
if(preferences.isDBUpdating(true))
//throw new UnsupportedOperationException("DB is updating");
return null;
SQLiteDatabase db = appDBHelper.getReadableDatabase();
List parts = uri.getPathSegments();
switch (sUriMatcher.match(uri)){
case LOCATION_SEARCH:
//authority/stops/location/"Lat"/"Lon"/"distance"
//distance in metres (integer)
if(parts.size()>=4 && "location".equals(parts.get(1))){
Double latitude = Double.parseDouble(parts.get(2));
Double longitude = Double.parseDouble(parts.get(3));
//converting distance to a float to not lose precision
float distance = parts.size()>=5 ? Float.parseFloat(parts.get(4))/1000 : 0.1f;
if(parts.size()>=5)
Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m");
Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres
String whereClause = StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND "
+StopsTable.COL_LAT +" > "+(latitude-distasAngle)+" AND "+
StopsTable.COL_LONG+" < "+(longitude+distasAngle)+" AND "+StopsTable.COL_LONG+" > "+(longitude-distasAngle);
//Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause);
return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null);
}
else {
Log.w(DEBUG_TAG,"Not enough parameters");
if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s);
return null;
}
case FAVORITES_OP:
- final String stopFavSelection = UserDB.getFavoritesColumnNamesAsArray[0]+" = ?";
+ final String stopFavSelection = getFavoritesColumnNamesAsArray[0]+" = ?";
db = userDBHelper.getReadableDatabase();
Log.d(DEBUG_TAG,"Asked information on Favorites about stop with id "+uri.getLastPathSegment());
return db.query(UserDB.TABLE_NAME,projection,stopFavSelection,new String[]{uri.getLastPathSegment()},null,null,sortOrder);
case STOP_OP:
//Let's try this plain and simple
final String[] selectionValues = {uri.getLastPathSegment()};
final String stopSelection = StopsTable.COL_ID+" = ?";
Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]);
return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder);
+ case MANY_STOPS:
+ return db.query(StopsTable.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
+
+ case GET_ALL_FAVORITES:
+ db = userDBHelper.getReadableDatabase();
+ return db.query(UserDB.TABLE_NAME, projection, selection, selectionArgs, null, null,sortOrder);
+
default:
- Log.d("DataProvider","got request "+uri.getPath()+" which doesn't match anything");
+ Log.e("DataProvider","got request "+uri.getPath()+" which doesn't match anything");
}
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
// public static Uri getBaseUriGivenOp(int operationType);
public static Uri.Builder getUriBuilderToComplete(){
final Uri.Builder b = new Uri.Builder();
b.scheme("content").authority(AUTHORITY);
return b;
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
}
diff --git a/src/it/reyboz/bustorino/data/CustomAsyncQueryHandler.java b/src/it/reyboz/bustorino/data/CustomAsyncQueryHandler.java
new file mode 100644
index 0000000..e92dcb4
--- /dev/null
+++ b/src/it/reyboz/bustorino/data/CustomAsyncQueryHandler.java
@@ -0,0 +1,45 @@
+package it.reyboz.bustorino.data;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+
+import java.lang.ref.WeakReference;
+
+public class CustomAsyncQueryHandler extends AsyncQueryHandler {
+
+ private WeakReference mListener;
+
+ public interface AsyncQueryListener {
+ void onQueryComplete(int token, Object cookie, Cursor cursor);
+ }
+
+ public CustomAsyncQueryHandler(ContentResolver cr, AsyncQueryListener listener) {
+ super(cr);
+ mListener = new WeakReference(listener);
+ }
+
+ public CustomAsyncQueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ /**
+ * Assign the given {@link AsyncQueryListener} to receive query events from
+ * asynchronous calls. Will replace any existing listener.
+ */
+ public void setQueryListener(AsyncQueryListener listener) {
+ mListener = new WeakReference(listener);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ final AsyncQueryListener listener = mListener.get();
+ if (listener != null) {
+ listener.onQueryComplete(token, cookie, cursor);
+ } else if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+}
diff --git a/src/it/reyboz/bustorino/data/FavoritesLiveData.java b/src/it/reyboz/bustorino/data/FavoritesLiveData.java
new file mode 100644
index 0000000..82b38c9
--- /dev/null
+++ b/src/it/reyboz/bustorino/data/FavoritesLiveData.java
@@ -0,0 +1,201 @@
+package it.reyboz.bustorino.data;
+
+import android.annotation.SuppressLint;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContentResolverCompat;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+import androidx.lifecycle.LiveData;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import it.reyboz.bustorino.backend.Route;
+import it.reyboz.bustorino.backend.Stop;
+
+import it.reyboz.bustorino.data.NextGenDB.Contract.*;
+
+public class FavoritesLiveData extends LiveData> implements CustomAsyncQueryHandler.AsyncQueryListener {
+ private static final String TAG = "FavoritesLiveData";
+ private final boolean notifyChangesDescendants;
+
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final FavoritesLiveData.ForceLoadContentObserver mObserver;
+ private final CustomAsyncQueryHandler queryHandler;
+
+
+ private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
+ AppDataProvider.FAVORITES).build();
+
+
+ private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=90;
+
+
+ @Nullable
+ private List stopsFromFavorites, stopsDone;
+
+ private boolean isQueryRunning = false;
+ private int stopNeededCount = 0;
+
+ public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) {
+ super();
+ mContext = context.getApplicationContext();
+ mObserver = new FavoritesLiveData.ForceLoadContentObserver();
+ notifyChangesDescendants = notifyDescendantsChanges;
+ queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this);
+
+ }
+
+ private void loadData() {
+ loadData(false);
+ }
+ private static Uri.Builder getStopsBuilder(){
+ return AppDataProvider.getUriBuilderToComplete().appendPath("stop");
+
+ }
+
+ private void loadData(boolean forceQuery) {
+ Log.d(TAG, "loadData()");
+
+ if (!forceQuery){
+ if (getValue()!= null){
+ //Data already loaded
+ return;
+ }
+ }
+ if (isQueryRunning){
+ //we are waiting for data, we will get an update soon
+ return;
+ }
+
+ isQueryRunning = true;
+ queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null);
+
+
+ }
+
+ @Override
+ protected void onActive() {
+ Log.d(TAG, "onActive()");
+ loadData();
+ }
+
+ @Override
+ protected void onInactive() {
+ Log.d(TAG, "onInactive()");
+
+ }
+
+ /**
+ * Clear the data for the cursor
+ */
+ public void onClear(){
+
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.unregisterContentObserver(mObserver);
+
+ }
+
+
+ @Override
+ protected void setValue(List stops) {
+
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver);
+
+ super.setValue(stops);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (token == FAV_TOKEN) {
+ stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.getFavoritesColumnNamesAsArray);
+ cursor.close();
+
+ for (int i = 0; i < stopsFromFavorites.size(); i++) {
+ Stop s = stopsFromFavorites.get(i);
+ queryHandler.startQuery(STOPS_TOKEN_BASE + i, null, getStopsBuilder().appendPath(s.ID).build(),
+ NextGenDB.QUERY_COLUMN_stops_all, null, null, null);
+ }
+ stopNeededCount = stopsFromFavorites.size();
+ stopsDone = new ArrayList<>();
+
+
+ } else if(token >= STOPS_TOKEN_BASE){
+ final int index = token - STOPS_TOKEN_BASE;
+ assert stopsFromFavorites != null;
+ Stop stopUpdate = stopsFromFavorites.get(index);
+ Stop finalStop;
+
+ List result = Arrays.asList(NextGenDB.getStopsFromCursorAllFields(cursor));
+ cursor.close();
+ if (result.size() < 1){
+ // stop is not in the DB
+ finalStop = stopUpdate;
+ } else{
+ finalStop = result.get(0);
+ assert (finalStop.ID.equals(stopUpdate.ID));
+ finalStop.setStopUserName(stopUpdate.getStopUserName());
+ }
+ if (stopsDone!=null)
+ stopsDone.add(finalStop);
+
+ stopNeededCount--;
+ if (stopNeededCount == 0) {
+ // we have finished the queries
+ isQueryRunning = false;
+ Collections.sort(stopsDone);
+
+ setValue(stopsDone);
+ }
+
+ }
+ }
+
+
+ /**
+ * Content Observer that forces reload of cursor when data changes
+ * On different thread (new Handler)
+ */
+ public final class ForceLoadContentObserver
+ extends ContentObserver {
+
+ public ForceLoadContentObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Log.d(TAG, "ForceLoadContentObserver.onChange()");
+ loadData(true);
+ }
+
+ }
+
+
+}
+
diff --git a/src/it/reyboz/bustorino/data/FavoritesViewModel.java b/src/it/reyboz/bustorino/data/FavoritesViewModel.java
new file mode 100644
index 0000000..9b78f0a
--- /dev/null
+++ b/src/it/reyboz/bustorino/data/FavoritesViewModel.java
@@ -0,0 +1,36 @@
+package it.reyboz.bustorino.data;
+
+import android.app.Application;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+import it.reyboz.bustorino.backend.Stop;
+
+public class FavoritesViewModel extends AndroidViewModel {
+
+ FavoritesLiveData favoritesLiveData;
+ final Context appContext;
+
+ public FavoritesViewModel(@NonNull Application application) {
+ super(application);
+ appContext = application.getApplicationContext();
+ }
+
+ @Override
+ protected void onCleared() {
+ favoritesLiveData.onClear();
+ super.onCleared();
+ }
+
+ public LiveData> getFavorites(){
+ if (favoritesLiveData==null){
+ favoritesLiveData= new FavoritesLiveData(appContext, true);
+ }
+ return favoritesLiveData;
+ }
+}
diff --git a/src/it/reyboz/bustorino/data/NextGenDB.java b/src/it/reyboz/bustorino/data/NextGenDB.java
index 497b7de..c917312 100644
--- a/src/it/reyboz/bustorino/data/NextGenDB.java
+++ b/src/it/reyboz/bustorino/data/NextGenDB.java
@@ -1,348 +1,356 @@
/*
BusTO (middleware)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.ContentValues;
import android.content.Context;
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.util.Log;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import java.util.*;
import static it.reyboz.bustorino.data.NextGenDB.Contract.*;
public class NextGenDB extends SQLiteOpenHelper{
public static final String DATABASE_NAME = "bustodatabase.db";
public static final int DATABASE_VERSION = 2;
public static final String DEBUG_TAG = "NextGenDB-BusTO";
//NO Singleton instance
//private static volatile NextGenDB instance = null;
//Some generating Strings
private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+
Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+
Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+
"UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )";
private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+
Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+
Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+
Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+
//SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary
Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+
//DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF
BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+
BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+
"FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") "
+")";
private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+
Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+
Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+
"PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") "
+")";
private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
- private static final String[] QUERY_COLUMN_stops_all = {
+ public static final String[] QUERY_COLUMN_stops_all = {
StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_LOCATION,
StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING};
- private static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " +
+ public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " +
StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG +
" >= ? AND "+ StopsTable.COL_LONG + " <= ?";
+ public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?";
- private Context appContext;
+
+ private final Context appContext;
public NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+
SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE);
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(oldVersion<2 && newVersion == 2){
//DROP ALL TABLES
db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME);
db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME);
//RECREATE THE TABLES WITH THE NEW SCHEMA
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
DatabaseUpdateService.startDBUpdate(appContext,0,true);
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.execSQL("PRAGMA foreign_keys=ON");
}
public static String getSqlCreateStopsTable(String tableName){
return "CREATE TABLE "+tableName+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
}
/**
* Query some bus stops inside a map view
*
* You can obtain the coordinates from OSMDroid using something like this:
* BoundingBoxE6 bb = mMapView.getBoundingBox();
* double latFrom = bb.getLatSouthE6() / 1E6;
* double latTo = bb.getLatNorthE6() / 1E6;
* double lngFrom = bb.getLonWestE6() / 1E6;
* double lngTo = bb.getLonEastE6() / 1E6;
*/
public synchronized Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
Stop[] stops = new Stop[0];
SQLiteDatabase db = this.getReadableDatabase();
Cursor result;
int count;
// coordinates must be strings in the where condition
String minLatRaw = String.valueOf(minLat);
String maxLatRaw = String.valueOf(maxLat);
String minLngRaw = String.valueOf(minLng);
String maxLngRaw = String.valueOf(maxLng);
- String[] queryColumns = {};
- String stopID;
- Route.Type type;
if(db == null) {
return stops;
}
try {
result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE,
new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw},
null, null, null);
-
- int colID = result.getColumnIndex(StopsTable.COL_ID);
- int colName = result.getColumnIndex(StopsTable.COL_NAME);
- int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
- int colType = result.getColumnIndex(StopsTable.COL_TYPE);
- int colLat = result.getColumnIndex(StopsTable.COL_LAT);
- int colLon = result.getColumnIndex(StopsTable.COL_LONG);
- int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
-
- count = result.getCount();
- stops = new Stop[count];
-
- int i = 0;
- while(result.moveToNext()) {
-
- stopID = result.getString(colID);
- type = Route.getTypeFromSymbol(result.getString(colType));
- String lines = result.getString(colLines).trim();
-
- String locationSometimesEmpty = result.getString(colLocation);
- if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
- locationSometimesEmpty = null;
- }
-
- stops[i++] = new Stop(stopID, result.getString(colName), null,
- locationSometimesEmpty, type, splitLinesString(lines),
- result.getDouble(colLat), result.getDouble(colLon));
- }
+ stops = getStopsFromCursorAllFields(result);
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
return stops;
}
result.close();
db.close();
return stops;
}
+ /**
+ * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all}
+ * @param result cursor from query
+ * @return an Array of the stops found in the query
+ */
+ public static Stop[] getStopsFromCursorAllFields(Cursor result){
+ int colID = result.getColumnIndex(StopsTable.COL_ID);
+ int colName = result.getColumnIndex(StopsTable.COL_NAME);
+ int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
+ int colType = result.getColumnIndex(StopsTable.COL_TYPE);
+ int colLat = result.getColumnIndex(StopsTable.COL_LAT);
+ int colLon = result.getColumnIndex(StopsTable.COL_LONG);
+ int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
+
+ int count = result.getCount();
+ Stop[] stops = new Stop[count];
+
+ int i = 0;
+ while(result.moveToNext()) {
+
+ final String stopID = result.getString(colID).trim();
+ final Route.Type type = Route.getTypeFromSymbol(result.getString(colType));
+ String lines = result.getString(colLines).trim();
+
+ String locationSometimesEmpty = result.getString(colLocation);
+ if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
+ locationSometimesEmpty = null;
+ }
+
+ stops[i++] = new Stop(stopID, result.getString(colName), null,
+ locationSometimesEmpty, type, splitLinesString(lines),
+ result.getDouble(colLat), result.getDouble(colLon));
+ }
+ return stops;
+ }
+
/**
* Insert batch content, already prepared as
* @param content ContentValues array
* @return number of lines inserted
*/
public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException {
final SQLiteDatabase db = this.getWritableDatabase();
int success = 0;
db.beginTransaction();
for (final ContentValues cv : content) {
try {
db.replaceOrThrow(tableName, null, cv);
success++;
} catch (SQLiteConstraintException d){
Log.w("NextGenDB_Insert","Failed insert with FOREIGN KEY... \n"+d.getMessage());
} catch (Exception e) {
Log.w("NextGenDB_Insert", e);
}
}
db.setTransactionSuccessful();
db.endTransaction();
return success;
}
public static List splitLinesString(String linesStr){
return Arrays.asList(linesStr.split("\\s*,\\s*"));
}
public static final class Contract{
//Ok, I get it, it really is a pain in the ass..
// But it's the only way to have maintainable code
public interface DataTables {
String getTableName();
String[] getFields();
}
public static final class LinesTable implements BaseColumns, DataTables {
//The fields
public static final String TABLE_NAME = "lines";
public static final String COLUMN_NAME = "line_name";
public static final String COLUMN_DESCRIPTION = "line_description";
public static final String COLUMN_TYPE = "line_bacino";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE};
}
}
public static final class BranchesTable implements BaseColumns, DataTables {
public static final String TABLE_NAME = "branches";
public static final String COL_BRANCHID = "branchid";
public static final String COL_LINE = "lineid";
public static final String COL_DESCRIPTION = "branch_description";
public static final String COL_DIRECTION = "branch_direzione";
public static final String COL_FESTIVO = "branch_festivo";
public static final String COL_TYPE = "branch_type";
public static final String COL_LUN="runs_lun";
public static final String COL_MAR="runs_mar";
public static final String COL_MER="runs_mer";
public static final String COL_GIO="runs_gio";
public static final String COL_VEN="runs_ven";
public static final String COL_SAB="runs_sab";
public static final String COL_DOM="runs_dom";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION,
COL_DIRECTION,COL_FESTIVO,COL_TYPE,
COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM
};
}
}
public static final class ConnectionsTable implements DataTables {
public static final String TABLE_NAME = "connections";
public static final String COLUMN_BRANCH = "branchid";
public static final String COLUMN_STOP_ID = "stopid";
public static final String COLUMN_ORDER = "ordine";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER};
}
}
public static final class StopsTable implements DataTables {
public static final String TABLE_NAME = "stops";
public static final String COL_ID = "stopid"; //integer
public static final String COL_TYPE = "stop_type";
public static final String COL_NAME = "stop_name";
public static final String COL_LAT = "stop_latitude";
public static final String COL_LONG = "stop_longitude";
public static final String COL_LOCATION = "stop_location";
public static final String COL_PLACE = "stop_placeName";
public static final String COL_LINES_STOPPING = "stop_lines";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING};
}
}
}
public static final class DBUpdatingException extends Exception{
public DBUpdatingException(String message) {
super(message);
}
}
}
diff --git a/src/it/reyboz/bustorino/data/StopsDB.java b/src/it/reyboz/bustorino/data/StopsDB.java
index ab94dff..9ea5bf1 100644
--- a/src/it/reyboz/bustorino/data/StopsDB.java
+++ b/src/it/reyboz/bustorino/data/StopsDB.java
@@ -1,308 +1,279 @@
/*
BusTO ("backend" components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsDBInterface;
public class StopsDB extends SQLiteAssetHelper implements StopsDBInterface {
private static String QUERY_TABLE_stops = "stops";
private static String QUERY_WHERE_ID = "ID = ?";
private static String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = "lat >= ? AND lat <= ? AND lon >= ? AND lon <= ?";
private static String[] QUERY_COLUMN_name = {"name"};
private static final String[] QUERY_COLUMN_location = {"location"};
private static final String[] QUERY_COLUMN_route = {"route"};
private static final String[] QUERY_COLUMN_everything = {"name", "location", "type", "lat", "lon"};
private static final String[] QUERY_COLUMN_everything_and_ID = {"ID", "name", "location", "type", "lat", "lon"};
private static String DB_NAME = "stops.sqlite";
private static int DB_VERSION = 1;
private SQLiteDatabase db;
private AtomicInteger openCounter = new AtomicInteger();
public StopsDB(Context context) {
super(context, DB_NAME, null, DB_VERSION);
// WARNING: do not remove the following line, do not save anything in this database, it will be overwritten on every update!
setForcedUpgrade();
// remove old database (BusTo version 1.8.5 and below)
File filename = new File(context.getFilesDir(), "busto.sqlite");
if(filename.exists()) {
//noinspection ResultOfMethodCallIgnored
filename.delete();
}
}
/**
* Through the magic of an atomic counter, the database gets opened and closed without race
* conditions between threads (HOPEFULLY).
*
* @return database or null if cannot be opened
*/
@Nullable
public synchronized SQLiteDatabase openIfNeeded() {
openCounter.incrementAndGet();
this.db = getReadableDatabase();
return this.db;
}
/**
* Through the magic of an atomic counter, the database gets really closed only when no thread
* is using it anymore (HOPEFULLY).
*/
public synchronized void closeIfNeeded() {
// is anybody still using the database or can we close it?
if(openCounter.decrementAndGet() <= 0) {
super.close();
this.db = null;
}
}
public List getRoutesByStop(@NonNull String stopID) {
String[] uselessArray = {stopID};
int count;
Cursor result;
if(this.db == null) {
return null;
}
try {
result = this.db.query("routemap", QUERY_COLUMN_route, "stop = ?", uselessArray, null, null, null);
} catch(SQLiteException e) {
return null;
}
count = result.getCount();
if(count == 0) {
return null;
}
List routes = new ArrayList<>(count);
while(result.moveToNext()) {
routes.add(result.getString(0));
}
result.close();
return routes;
}
- public String getNameFromID(@NonNull String stopID) {
- String[] uselessArray = {stopID};
- int count;
- String name;
- Cursor result;
-
- if(this.db == null) {
- return null;
- }
-
- try {
- result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_name, QUERY_WHERE_ID, uselessArray, null, null, null);
- } catch(SQLiteException e) {
- return null;
- }
-
- count = result.getCount();
- if(count == 0) {
- return null;
- }
-
- result.moveToNext();
- name = result.getString(0);
-
- result.close();
-
- return name;
- }
-
public String getLocationFromID(@NonNull String stopID) {
String[] uselessArray = {stopID};
int count;
String name;
Cursor result;
if(this.db == null) {
return null;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_location, QUERY_WHERE_ID, uselessArray, null, null, null);
} catch(SQLiteException e) {
return null;
}
count = result.getCount();
if(count == 0) {
return null;
}
result.moveToNext();
name = result.getString(0);
result.close();
return name;
}
public Stop getAllFromID(@NonNull String stopID) {
Cursor result;
int count;
Stop s;
if(this.db == null) {
return null;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_everything, QUERY_WHERE_ID, new String[] {stopID}, null, null, null);
int colName = result.getColumnIndex("name");
int colLocation = result.getColumnIndex("location");
int colType = result.getColumnIndex("type");
int colLat = result.getColumnIndex("lat");
int colLon = result.getColumnIndex("lon");
count = result.getCount();
if(count == 0) {
return null;
}
result.moveToNext();
Route.Type type = routeTypeFromSymbol(result.getString(colType));
String locationWhichSometimesIsAnEmptyString = result.getString(colLocation);
if(locationWhichSometimesIsAnEmptyString.length() <= 0) {
locationWhichSometimesIsAnEmptyString = null;
}
s = new Stop(stopID, result.getString(colName), null, locationWhichSometimesIsAnEmptyString, type, getRoutesByStop(stopID), result.getDouble(colLat), result.getDouble(colLon));
} catch(SQLiteException e) {
return null;
}
result.close();
return s;
}
/**
* Query some bus stops inside a map view
*
* You can obtain the coordinates from OSMDroid using something like this:
* BoundingBoxE6 bb = mMapView.getBoundingBox();
* double latFrom = bb.getLatSouthE6() / 1E6;
* double latTo = bb.getLatNorthE6() / 1E6;
* double lngFrom = bb.getLonWestE6() / 1E6;
* double lngTo = bb.getLonEastE6() / 1E6;
*/
public Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
Stop[] stops = new Stop[0];
Cursor result;
int count;
// coordinates must be strings in the where condition
String minLatRaw = String.valueOf(minLat);
String maxLatRaw = String.valueOf(maxLat);
String minLngRaw = String.valueOf(minLng);
String maxLngRaw = String.valueOf(maxLng);
String stopID;
Route.Type type;
if(this.db == null) {
return stops;
}
try {
result = this.db.query(QUERY_TABLE_stops, QUERY_COLUMN_everything_and_ID, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null);
int colID = result.getColumnIndex("ID");
int colName = result.getColumnIndex("name");
int colLocation = result.getColumnIndex("location");
int colType = result.getColumnIndex("type");
int colLat = result.getColumnIndex("lat");
int colLon = result.getColumnIndex("lon");
count = result.getCount();
stops = new Stop[count];
int i = 0;
while(result.moveToNext()) {
stopID = result.getString(colID);
type = routeTypeFromSymbol(result.getString(colType));
String locationWhichSometimesIsAnEmptyString = result.getString(colLocation);
if (locationWhichSometimesIsAnEmptyString.length() <= 0) {
locationWhichSometimesIsAnEmptyString = null;
}
stops[i++] = new Stop(stopID, result.getString(colName), null,
locationWhichSometimesIsAnEmptyString, type, getRoutesByStop(stopID),
result.getDouble(colLat), result.getDouble(colLon));
}
} catch(SQLiteException e) {
// TODO: put a warning in the log
return stops;
}
result.close();
return stops;
}
/**
* Get a Route Type from its char symbol
*
* @param route The route symbol (e.g. "B")
* @return The related Route.Type (e.g. Route.Type.Bus)
*/
public static Route.Type routeTypeFromSymbol(String route) {
switch (route) {
case "M":
return Route.Type.METRO;
case "T":
return Route.Type.RAILWAY;
}
// default with case "B"
return Route.Type.BUS;
}
}
diff --git a/src/it/reyboz/bustorino/data/UserDB.java b/src/it/reyboz/bustorino/data/UserDB.java
index 731c135..a6f19ee 100644
--- a/src/it/reyboz/bustorino/data/UserDB.java
+++ b/src/it/reyboz/bustorino/data/UserDB.java
@@ -1,290 +1,320 @@
/*
BusTO ("backend" components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.content.Context;
+import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsDBInterface;
public class UserDB extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "user.db";
static final String TABLE_NAME = "favorites";
private final Context c; // needed during upgrade
private final static String[] usernameColumnNameAsArray = {"username"};
public final static String[] getFavoritesColumnNamesAsArray = {"ID", "username"};
- public UserDB(Context context) {
+ private static final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
+ AppDataProvider.FAVORITES).build();
+
+
+ public UserDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.c = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
// exception intentionally left unhandled
db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)");
if(OldDB.doesItExist(this.c)) {
upgradeFromOldDatabase(db);
}
}
private void upgradeFromOldDatabase(SQLiteDatabase newdb) {
OldDB old;
try {
old = new OldDB(this.c);
} catch(IllegalStateException e) {
// can't create database => it doesn't really exist, no matter what doesItExist() says
return;
}
int ver = old.getOldVersion();
/* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app
* has crashed midway through the upgrade and the user is retrying, that should never show
* up here. And if it does, try to recover favorites anyway.
* Versions < 8 already got dropped during the update process, so let's do the same.
*
* Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it
* more complicated. Workaround added in OldDB.
*/
if(ver >= 8) {
ArrayList ID = new ArrayList<>();
ArrayList username = new ArrayList<>();
int len;
int len2;
try {
Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {});
int zero = c.getColumnIndex("busstop_ID");
int one = c.getColumnIndex("busstop_username");
while(c.moveToNext()) {
try {
ID.add(c.getString(zero));
} catch(Exception e) {
// no ID = can't add this
continue;
}
if(c.getString(one) == null || c.getString(one).length() <= 0) {
username.add(null);
} else {
username.add(c.getString(one));
}
}
c.close();
old.close();
} catch(Exception ignored) {
// there's no hope, go ahead and nuke old database.
}
len = ID.size();
len2 = username.size();
if(len2 < len) {
len = len2;
}
if (len > 0) {
try {
for (int i = 0; i < len; i++) {
final Stop mStop = new Stop(ID.get(i));
mStop.setStopUserName(username.get(i));
addOrUpdateStop(mStop, newdb);
}
} catch(Exception ignored) {
// partial data is better than no data at all, no transactions here
}
}
}
if(!OldDB.destroy(this.c)) {
// TODO: notify user somehow?
Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user.");
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// nothing to do yet
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// nothing to do yet
}
/**
* Check if a stop ID is in the favorites
*
* @param db readable database
* @param stopId stop ID
* @return boolean
*/
public static boolean isStopInFavorites(SQLiteDatabase db, String stopId) {
boolean found = false;
try {
Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopId}, null, null, null);
if(c.moveToNext()) {
found = true;
}
c.close();
} catch(SQLiteException ignored) {
// don't care
}
return found;
}
/**
* Gets stop name set by the user.
*
* @param db readable database
* @param stopID stop ID
* @return name set by user, or null if not set\not found
*/
public static String getStopUserName(SQLiteDatabase db, String stopID) {
String username = null;
try {
Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null);
if(c.moveToNext()) {
username = c.getString(c.getColumnIndex("username"));
}
c.close();
} catch(SQLiteException ignored) {}
return username;
}
/**
* Get all the bus stops marked as favorites
*
* @param db
* @param dbi
* @return
*/
public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) {
List l = new ArrayList<>();
Stop s;
String stopID, stopUserName;
try {
Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null);
int colID = c.getColumnIndex("ID");
int colUser = c.getColumnIndex("username");
while(c.moveToNext()) {
stopUserName = c.getString(colUser);
stopID = c.getString(colID);
s = dbi.getAllFromID(stopID);
if(s == null) {
// can't find it in database
l.add(new Stop(stopUserName, stopID, null, null, null));
} else {
// setStopName() already does sanity checks
s.setStopUserName(stopUserName);
l.add(s);
}
}
-
c.close();
} catch(SQLiteException ignored) {}
// comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database
Collections.sort(l);
return l;
}
+ public static void notifyContentProvider(Context context){
+ context.
+ getContentResolver().
+ notifyChange(FAVORITES_URI, null);
+ }
+
+ public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){
+ List colsList = Arrays.asList(columns);
+ if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){
+ throw new IllegalArgumentException();
+ }
+ ArrayList l = new ArrayList<>();
+ final int colID = cursor.getColumnIndex("ID");
+ final int colUser = cursor.getColumnIndex("username");
+ while(cursor.moveToNext()) {
+ final String stopUserName = cursor.getString(colUser);
+ final String stopID = cursor.getString(colID);
+ final Stop s = new Stop(stopID.trim());
+ if (stopUserName!=null) s.setStopUserName(stopUserName);
+
+ l.add(s);
+ }
+ return l;
+
+ }
public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) {
ContentValues cv = new ContentValues();
long result = -1;
String un = s.getStopUserName();
cv.put("ID", s.ID);
// is there an username?
if(un == null) {
// no: see if it's in the database
cv.put("username", getStopUserName(db, s.ID));
} else {
// yes: use it
cv.put("username", un);
}
try {
//ignore and throw -1 if the row is already in the DB
result = db.insertWithOnConflict(TABLE_NAME, null, cv,SQLiteDatabase.CONFLICT_IGNORE);
} catch (SQLiteException ignored) {}
// Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return
return (result != -1) || updateStop(s, db);
}
public static boolean updateStop(Stop s, SQLiteDatabase db) {
try {
ContentValues cv = new ContentValues();
cv.put("username", s.getStopUserName());
db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID});
return true;
} catch(SQLiteException e) {
return false;
}
}
public static boolean deleteStop(Stop s, SQLiteDatabase db) {
try {
db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID});
return true;
} catch(SQLiteException e) {
return false;
}
}
public static boolean checkStopInFavorites(String stopID, Context con){
boolean found = false;
// no stop no party
if (stopID != null) {
SQLiteDatabase userDB = new UserDB(con).getReadableDatabase();
found = UserDB.isStopInFavorites(userDB, stopID);
}
return found;
}
}
diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
index 00f856c..2abc239 100644
--- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
@@ -1,487 +1,505 @@
/*
BusTO - Fragments components
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.PalinaAdapter;
import it.reyboz.bustorino.backend.ArrivalsFetcher;
import it.reyboz.bustorino.backend.DBStatusManager;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.FiveTAPIFetcher;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.FiveTScraperFetcher;
import it.reyboz.bustorino.backend.GTTJSONFetcher;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.AppDataProvider;
import it.reyboz.bustorino.data.NextGenDB;
import it.reyboz.bustorino.data.UserDB;
import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction;
public class ArrivalsFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks {
private final static String KEY_STOP_ID = "stopid";
private final static String KEY_STOP_NAME = "stopname";
- private final static String DEBUG_TAG = "BUSTOArrivalsFragment";
+ private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment";
+ private String DEBUG_TAG = DEBUG_TAG_ALL;
private final static int loaderFavId = 2;
private final static int loaderStopId = 1;
private final static ArrivalsFetcher[] defaultFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()};
static final String STOP_TITLE = "messageExtra";
private @Nullable String stopID,stopName;
private DBStatusManager prefs;
private DBStatusManager.OnDBUpdateStatusChangeListener listener;
private boolean justCreated = false;
private Palina lastUpdatedPalina = null;
private boolean needUpdateOnAttach = false;
private boolean fetchersChangeRequestPending = false;
private boolean stopIsInFavorites = false;
//Views
protected ImageButton addToFavorites;
protected TextView timesSourceTextView;
private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers));
+ private boolean reloadOnResume = true;
public static ArrivalsFragment newInstance(String stopID){
return newInstance(stopID, null);
}
public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){
ArrivalsFragment fragment = new ArrivalsFragment();
Bundle args = new Bundle();
args.putString(KEY_STOP_ID,stopID);
- //parameter for ResultListFragment
+ //parameter for ResultListFragmentrequestArrivalsForStopID
args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS);
if (stopName != null){
args.putString(KEY_STOP_NAME,stopName);
}
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
stopID = getArguments().getString(KEY_STOP_ID);
+ DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID;
+
//this might really be null
stopName = getArguments().getString(KEY_STOP_NAME);
final ArrivalsFragment arrivalsFragment = this;
listener = new DBStatusManager.OnDBUpdateStatusChangeListener() {
@Override
public void onDBStatusChanged(boolean updating) {
if(!updating){
getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment);
} else {
final LoaderManager lm = getLoaderManager();
lm.destroyLoader(loaderFavId);
lm.destroyLoader(loaderStopId);
}
}
@Override
public boolean defaultStatusValue() {
return true;
}
};
prefs = new DBStatusManager(getContext().getApplicationContext(),listener);
justCreated = true;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_arrivals, container, false);
messageTextView = (TextView) root.findViewById(R.id.messageTextView);
addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites);
resultsListView = (ListView) root.findViewById(R.id.resultsListView);
timesSourceTextView = (TextView) root.findViewById(R.id.timesSourceTextView);
timesSourceTextView.setOnLongClickListener(view -> {
if(!fetchersChangeRequestPending){
rotateFetchers();
//Show we are changing provider
timesSourceTextView.setText(R.string.arrival_source_changing);
mListener.requestArrivalsForStopID(stopID);
fetchersChangeRequestPending = true;
return true;
}
return false;
});
timesSourceTextView.setOnClickListener(view -> {
Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT)
.show();
});
//Button
addToFavorites.setClickable(true);
addToFavorites.setOnClickListener(v -> {
// add/remove the stop in the favorites
toggleLastStopToFavorites();
});
resultsListView.setOnItemClickListener((parent, view, position, id) -> {
String routeName;
Route r = (Route) parent.getItemAtPosition(position);
routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay());
if (routeName == null) {
routeName = r.getNameForDisplay();
}
if (r.destinazione == null || r.destinazione.length() == 0) {
Toast.makeText(getContext(),
getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(),
getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show();
}
});
String displayName = getArguments().getString(STOP_TITLE);
setTextViewMessage(String.format(
getString(R.string.passages), displayName));
String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW);
if (probablemessage != null) {
//Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage);
messageTextView.setText(probablemessage);
messageTextView.setVisibility(View.VISIBLE);
}
return root;
}
@Override
public void onResume() {
super.onResume();
LoaderManager loaderManager = getLoaderManager();
-
+ Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated);
if(stopID!=null){
//refresh the arrivals
- if(!justCreated)
- mListener.requestArrivalsForStopID(stopID);
+ if(!justCreated){
+ if (reloadOnResume)
+ mListener.requestArrivalsForStopID(stopID);
+ }
else justCreated = false;
//start the loader
if(prefs.isDBUpdating(true)){
prefs.registerListener();
} else {
+ Log.d(DEBUG_TAG, "Restarting loader for stop");
loaderManager.restartLoader(loaderFavId, getArguments(), this);
}
updateMessage();
}
}
+
@Override
public void onStart() {
super.onStart();
if (needUpdateOnAttach){
updateFragmentData(null);
}
}
+ @Override
+ public void onPause() {
+ if(listener!=null)
+ prefs.unregisterListener();
+ super.onPause();
+ LoaderManager loaderManager = getLoaderManager();
+ Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders());
+ loaderManager.destroyLoader(loaderFavId);
+
+ }
@Nullable
public String getStopID() {
return stopID;
}
+ public boolean reloadsOnResume() {
+ return reloadOnResume;
+ }
+
+ public void setReloadOnResume(boolean reloadOnResume) {
+ this.reloadOnResume = reloadOnResume;
+ }
+
/**
* Give the fetchers
* @return the list of the fetchers
*/
public ArrayList getCurrentFetchers(){
ArrayList v = new ArrayList();
for (ArrivalsFetcher fetcher: fetchers){
v.add(fetcher);
}
return v;
}
public Fetcher[] getCurrentFetchersAsArray(){
Fetcher[] arr = new Fetcher[fetchers.size()];
fetchers.toArray(arr);
return arr;
}
private void rotateFetchers(){
Collections.rotate(fetchers, -1);
}
/**
* Update the UI with the new data
* @param p the full Palina
*/
public void updateFragmentData(@Nullable Palina p){
if (p!=null)
lastUpdatedPalina = p;
if (!isAdded()){
//defer update at next show
if (p==null)
Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null");
else needUpdateOnAttach = true;
} else {
final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina);
showArrivalsSources(lastUpdatedPalina);
super.resetListAdapter(adapter);
}
}
/**
* Set the message of the arrival times source
* @param p Palina with the arrival times
*/
protected void showArrivalsSources(Palina p){
final Passaggio.Source source = p.getPassaggiSourceIfAny();
if (source == null){
Log.e(DEBUG_TAG, "NULL SOURCE");
return;
}
String source_txt;
switch (source){
case GTTJSON:
source_txt = getString(R.string.gttjsonfetcher);
break;
case FiveTAPI:
source_txt = getString(R.string.fivetapifetcher);
break;
case FiveTScraper:
source_txt = getString(R.string.fivetscraper);
break;
case UNDETERMINED:
//Don't show the view
timesSourceTextView.setVisibility(View.GONE);
return;
default:
throw new IllegalStateException("Unexpected value: " + source);
}
int count = 0;
while (source != fetchers.get(0).getSourceForFetcher() && count < 100){
//we need to update the fetcher that is requested
rotateFetchers();
count++;
}
if (count>10)
Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work");
final String base_message = getString(R.string.times_source_fmt, source_txt);
timesSourceTextView.setVisibility(View.VISIBLE);
timesSourceTextView.setText(base_message);
fetchersChangeRequestPending = false;
}
@Override
public void setNewListAdapter(ListAdapter adapter) {
throw new UnsupportedOperationException();
}
/**
* Update the message in the fragment
*
* It may eventually change the "Add to Favorite" icon
*/
private void updateMessage(){
String message = null;
if (stopName != null && stopID != null && stopName.length() > 0) {
message = (stopID.concat(" - ").concat(stopName));
} else if(stopID!=null) {
message = stopID;
} else {
Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong");
}
if(message!=null) {
setTextViewMessage(getString(R.string.passages,message));
}
// whatever is the case, update the star icon
//updateStarIconFromLastBusStop();
}
@NonNull
@Override
public Loader onCreateLoader(int id, Bundle args) {
if(args.getString(KEY_STOP_ID)==null) return null;
final String stopID = args.getString(KEY_STOP_ID);
final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete();
CursorLoader cl;
switch (id){
case loaderFavId:
builder.appendPath("favorites").appendPath(stopID);
cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null);
break;
case loaderStopId:
builder.appendPath("stop").appendPath(stopID);
cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME},
null,null,null);
break;
default:
return null;
}
cl.setUpdateThrottle(500);
return cl;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
switch (loader.getId()){
case loaderFavId:
final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]);
if(data.getCount()>0){
// IT'S IN FAVORITES
data.moveToFirst();
final String probableName = data.getString(colUserName);
stopIsInFavorites = true;
- if(probableName!=null && !probableName.isEmpty()){
- stopName = probableName;
- //update the message in the textview
- updateMessage();
- }
+ stopName = probableName;
+ //update the message in the textview
+ updateMessage();
+
} else {
stopIsInFavorites =false;
}
updateStarIcon();
if(stopName == null){
//stop is not inside the favorites and wasn't provided
Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB");
getLoaderManager().restartLoader(loaderStopId,getArguments(),this);
}
break;
case loaderStopId:
if(data.getCount()>0){
data.moveToFirst();
stopName = data.getString(data.getColumnIndex(
NextGenDB.Contract.StopsTable.COL_NAME
));
updateMessage();
} else {
Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL");
}
}
}
- @Override
- public void onPause() {
- if(listener!=null)
- prefs.unregisterListener();
- super.onPause();
- }
-
@Override
public void onLoaderReset(Loader loader) {
//NOTHING TO DO
}
public void toggleLastStopToFavorites() {
Stop stop = lastUpdatedPalina;
if (stop != null) {
// toggle the status in background
new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE,
v->updateStarIconFromLastBusStop(v)).execute(stop);
} else {
// this case have no sense, but just immediately update the favorite icon
updateStarIconFromLastBusStop(true);
}
}
/**
* Update the star "Add to favorite" icon
*/
public void updateStarIconFromLastBusStop(Boolean toggleDone) {
if (stopIsInFavorites)
stopIsInFavorites = !toggleDone;
else stopIsInFavorites = toggleDone;
updateStarIcon();
// check if there is a last Stop
/*
if (stopID == null) {
addToFavorites.setVisibility(View.INVISIBLE);
} else {
// filled or outline?
if (isStopInFavorites(stopID)) {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
} else {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
addToFavorites.setVisibility(View.VISIBLE);
}
*/
}
/**
* Update the star icon according to `stopIsInFavorites`
*/
public void updateStarIcon() {
// no favorites no party!
// check if there is a last Stop
if (stopID == null) {
addToFavorites.setVisibility(View.INVISIBLE);
} else {
// filled or outline?
if (stopIsInFavorites) {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
} else {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
addToFavorites.setVisibility(View.VISIBLE);
}
}
}
diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java
new file mode 100644
index 0000000..22e79ca
--- /dev/null
+++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java
@@ -0,0 +1,272 @@
+package it.reyboz.bustorino.fragments;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+import it.reyboz.bustorino.ActivityFavorites;
+import it.reyboz.bustorino.ActivityMain;
+import it.reyboz.bustorino.ActivityMap;
+import it.reyboz.bustorino.R;
+import it.reyboz.bustorino.adapters.StopAdapter;
+import it.reyboz.bustorino.backend.Stop;
+import it.reyboz.bustorino.data.FavoritesViewModel;
+import it.reyboz.bustorino.data.UserDB;
+import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction;
+
+public class FavoritesFragment extends BaseFragment {
+
+ private ListView favoriteListView;
+ private EditText busStopNameText;
+ private TextView favoriteTipTextView;
+ private ImageView angeryBusImageView;
+
+ @Nullable
+ private CommonFragmentListener mListener;
+
+
+
+
+ public static FavoritesFragment newInstance() {
+ FavoritesFragment fragment = new FavoritesFragment();
+ Bundle args = new Bundle();
+ //args.putString(ARG_PARAM1, param1);
+ //args.putString(ARG_PARAM2, param2);
+ fragment.setArguments(args);
+ return fragment;
+ }
+ private FavoritesFragment(){
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getArguments() != null) {
+ //do nothing
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.fragment_favorites, container, false);
+ favoriteListView = root.findViewById(R.id.favoriteListView);
+ favoriteListView.setOnItemClickListener((parent, view, position, id) -> {
+ /**
+ * Casting because of Javamerda
+ * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener
+ */
+ Stop busStop = (Stop) parent.getItemAtPosition(position);
+
+ if(mListener!=null){
+ mListener.requestArrivalsForStopID(busStop.ID);
+ }
+
+ });
+ angeryBusImageView = root.findViewById(R.id.angeryBusImageView);
+ favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView);
+ registerForContextMenu(favoriteListView);
+
+ FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class);
+ model.getFavorites().observe(getViewLifecycleOwner(), this::showStops);
+ return root;
+ }
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof CommonFragmentListener) {
+ mListener = (CommonFragmentListener) context;
+ } else {
+ throw new RuntimeException(context.toString()
+ + " must implement CommonFragmentListener");
+ }
+
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mListener = null;
+ }
+
+ @Override
+ public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if (v.getId() == R.id.favoriteListView) {
+ // if we aren't attached to activity, return null
+ if (getActivity()==null) return;
+ MenuInflater inflater = getActivity().getMenuInflater();
+ inflater.inflate(R.menu.menu_favourites_entry, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ Stop busStop = (Stop) favoriteListView.getItemAtPosition(info.position);
+
+ switch (item.getItemId()) {
+ case R.id.action_favourite_entry_delete:
+ if (getContext()!=null)
+ new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE,
+ result -> {
+
+ }).execute(busStop);
+
+ return true;
+
+ case R.id.action_rename_bus_stop_username:
+ showBusStopUsernameInputDialog(busStop);
+ return true;
+ case R.id.action_view_on_map:
+ final String theGeoUrl = busStop.getGeoURL();
+ /*
+ if(theGeoUrl==null){
+ //doesn't have a position
+ Toast.makeText(getContext(),R.string.cannot_show_on_map_no_position,Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ // start ActivityMap with these extras in intent
+ Intent intent = new Intent(getContext(), ActivityMap.class);
+ Bundle b = new Bundle();
+ double lat, lon;
+ if (busStop.getLatitude()!=null)
+ lat = busStop.getLatitude();
+ else lat = 200;
+ if (busStop.getLongitude()!=null)
+ lon = busStop.getLongitude();
+ else lon = 200;
+ b.putDouble("lat", lat);
+ b.putDouble("lon",lon);
+ b.putString("name", busStop.getStopDefaultName());
+ b.putString("ID", busStop.ID);
+ intent.putExtras(b);
+
+ startActivity(intent);
+ TODO: start map on button press
+ */
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ void showStops(List busStops){
+ // If no data is found show a friendly message
+
+ if (busStops.size() == 0) {
+ favoriteListView.setVisibility(View.INVISIBLE);
+ // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView);
+ //assert favoriteTipTextView != null;
+ favoriteTipTextView.setVisibility(View.VISIBLE);
+ //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView);
+ angeryBusImageView.setVisibility(View.VISIBLE);
+ } else {
+ favoriteListView.setVisibility(View.VISIBLE);
+ favoriteTipTextView.setVisibility(View.INVISIBLE);
+ angeryBusImageView.setVisibility(View.INVISIBLE);
+ }
+ /* There's a nice method called notifyDataSetChanged() to avoid building the ListView
+ * all over again. This method exists in a billion answers on Stack Overflow, but
+ * it's nowhere to be seen around here, Android Studio can't find it no matter what.
+ * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I
+ * guess) and requires to modify the list with .add() and .clear() and some other
+ * methods, so to update a single stop we need to completely rebuild the list for no
+ * reason. It would probably end up as "slow" as throwing away the old ListView and
+ * redrwaing everything.
+ */
+
+ // Show results
+ favoriteListView.setAdapter(new StopAdapter(getContext(), busStops));
+ }
+
+ public void showBusStopUsernameInputDialog(final Stop busStop) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+
+ LayoutInflater inflater = this.getLayoutInflater();
+ View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null);
+
+ busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name);
+ busStopNameText.setText(busStop.getStopDisplayName());
+ busStopNameText.setHint(busStop.getStopDefaultName());
+
+ builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title));
+ builder.setView(renameDialogLayout);
+ builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String busStopUsername = busStopNameText.getText().toString();
+ String oldUserName = busStop.getStopUserName();
+
+ // changed to none
+ if(busStopUsername.length() == 0) {
+ // unless it was already empty, set new
+ if(oldUserName != null) {
+ busStop.setStopUserName(null);
+
+ }
+ } else { // changed to something
+ // something different?
+ if(!busStopUsername.equals(oldUserName)) {
+ busStop.setStopUserName(busStopUsername);
+
+ }
+ }
+ launchUpdate(busStop);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // delete user name from database
+ busStop.setStopUserName(null);
+ launchUpdate(busStop);
+
+ }
+ });
+ builder.show();
+ }
+
+ private void launchUpdate(Stop busStop){
+ if (getContext()!=null)
+ new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE,
+ new AsyncStopFavoriteAction.ResultListener() {
+ @Override
+ public void doStuffWithResult(Boolean result) {
+ //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show();
+ }
+ }).execute(busStop);
+ }
+}
diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
index 66063ec..4ff4e53 100644
--- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java
+++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
@@ -1,232 +1,267 @@
/*
BusTO (fragments)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.content.Context;
+
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.util.Log;
import android.widget.Toast;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
-import it.reyboz.bustorino.data.NextGenDB;
import it.reyboz.bustorino.middleware.*;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* Helper class to manage the fragments and their needs
*/
public class FragmentHelper {
//GeneralActivity act;
private final FragmentListenerMain listenerMain;
private final WeakReference managerWeakRef;
private Stop lastSuccessfullySearchedBusStop;
//support for multiple frames
private final int secondaryFrameLayout;
private final int primaryFrameLayout;
private final Context context;
public static final int NO_FRAME = -3;
private static final String DEBUG_TAG = "BusTO FragmHelper";
private WeakReference lastTaskRef;
private boolean shouldHaltAllActivities=false;
public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) {
this(listener,framan, context,mainFrame,NO_FRAME);
}
public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) {
this.listenerMain = listener;
this.managerWeakRef = new WeakReference<>(fraMan);
this.primaryFrameLayout = primaryFrameLayout;
this.secondaryFrameLayout = secondaryFrameLayout;
this.context = context.getApplicationContext();
}
/**
* Get the last successfully searched bus stop or NULL
*
* @return the stop
*/
public Stop getLastSuccessfullySearchedBusStop() {
return lastSuccessfullySearchedBusStop;
}
public void setLastSuccessfullySearchedBusStop(Stop stop) {
this.lastSuccessfullySearchedBusStop = stop;
}
public void setLastTaskRef(WeakReference lastTaskRef) {
this.lastTaskRef = lastTaskRef;
}
/**
* Called when you need to create a fragment for a specified Palina
* @param p the Stop that needs to be displayed
*/
- public void createOrUpdateStopFragment(Palina p){
+ public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){
boolean sameFragment;
ArrivalsFragment arrivalsFragment;
if(managerWeakRef.get()==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything");
return;
}
FragmentManager fm = managerWeakRef.get();
if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) {
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout);
//Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?");
+ assert arrivalsFragment != null;
sameFragment = arrivalsFragment.isFragmentForTheSameStop(p);
} else {
sameFragment = false;
Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment");
}
setLastSuccessfullySearchedBusStop(p);
if(!sameFragment) {
//set the String to be displayed on the fragment
String displayName = p.getStopDisplayName();
String displayStuff;
if (displayName != null && displayName.length() > 0) {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName);
} else {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID);
}
- attachFragmentToContainer(fm,arrivalsFragment,true,ResultListFragment.getFragmentTag(p));
+ String probableTag = ResultListFragment.getFragmentTag(p);
+ attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack));
} else {
Log.d("BusTO", "Same bus stop, accessing existing fragment");
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout);
}
// DO NOT CALL `setListAdapter` ever on arrivals fragment
arrivalsFragment.updateFragmentData(p);
+ // enable fragment auto refresh
+ arrivalsFragment.setReloadOnResume(true);
listenerMain.hideKeyboard();
toggleSpinner(false);
}
/**
* Called when you need to display the results of a search of stops
* @param resultList the List of stops found
* @param query String queried
*/
- public void createFragmentFor(List resultList,String query){
+ public void createStopListFragment(List resultList, String query, boolean addToBackStack){
listenerMain.hideKeyboard();
StopListFragment listfragment = StopListFragment.newInstance(query);
if(managerWeakRef.get()==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything");
return;
}
- attachFragmentToContainer(managerWeakRef.get(),listfragment,false,"search_"+query);
+ attachFragmentToContainer(managerWeakRef.get(),listfragment,
+ new AttachParameters("search_"+query, false,addToBackStack));
listfragment.setStopList(resultList);
toggleSpinner(false);
}
/**
* Wrapper for toggleSpinner in Activity
* @param on new status of spinner system
*/
public void toggleSpinner(boolean on){
listenerMain.toggleSpinner(on);
}
/**
* Attach a new fragment to a cointainer
* @param fm the FragmentManager
* @param fragment the Fragment
- * @param sendToSecondaryFrame needs to be displayed in secondary frame or not
- * @param tag tag for the fragment
+ * @param parameters attach parameters
*/
- public void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){
+ protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){
FragmentTransaction ft = fm.beginTransaction();
- if(sendToSecondaryFrame && secondaryFrameLayout!=NO_FRAME)
- ft.replace(secondaryFrameLayout,fragment,tag);
- else ft.replace(primaryFrameLayout,fragment,tag);
- ft.addToBackStack("state_"+tag);
+ int frameID;
+ if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME)
+ // ft.replace(secondaryFrameLayout,fragment,tag);
+ frameID = secondaryFrameLayout;
+ else frameID = primaryFrameLayout;
+ switch (parameters.transaction){
+ case REPLACE:
+ ft.replace(frameID,fragment,parameters.tag);
+
+ }
+ if (parameters.addToBackStack)
+ ft.addToBackStack("state_"+parameters.tag);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
ft.commit();
//fm.executePendingTransactions();
}
public void setBlockAllActivities(boolean shouldI) {
this.shouldHaltAllActivities = shouldI;
}
public void stopLastRequestIfNeeded(){
if(lastTaskRef == null) return;
AsyncDataDownload task = lastTaskRef.get();
if(task!=null){
task.cancel(true);
}
}
/**
* Wrapper to show the errors/status that happened
* @param res result from Fetcher
*/
public void showErrorMessage(Fetcher.result res){
//TODO: implement a common set of errors for all fragments
switch (res){
case OK:
break;
case CLIENT_OFFLINE:
showToastMessage(R.string.network_error, true);
break;
case SERVER_ERROR:
if (utils.isConnected(context)) {
showToastMessage(R.string.parsing_error, true);
} else {
showToastMessage(R.string.network_error, true);
}
case PARSER_ERROR:
default:
showShortToast(R.string.internal_error);
break;
case QUERY_TOO_SHORT:
showShortToast(R.string.query_too_short);
break;
case EMPTY_RESULT_SET:
showShortToast(R.string.no_bus_stop_have_this_name);
break;
}
}
public void showToastMessage(int messageID, boolean short_lenght) {
final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
if (context != null)
Toast.makeText(context, messageID, length).show();
}
private void showShortToast(int messageID){
showToastMessage(messageID, true);
}
+ enum Transaction{
+ REPLACE,
+ }
+ static final class AttachParameters {
+ String tag;
+ boolean attachToSecondaryFrame;
+ Transaction transaction;
+ boolean addToBackStack;
+
+ public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) {
+ this.tag = tag;
+ this.attachToSecondaryFrame = attachToSecondaryFrame;
+ this.transaction = transaction;
+ this.addToBackStack = addToBackStack;
+ }
+
+ public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) {
+ this.tag = tag;
+ this.attachToSecondaryFrame = attachToSecondaryFrame;
+ this.addToBackStack = addToBackStack;
+ this.transaction = Transaction.REPLACE;
+ }
+ }
}
diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
index 789d76a..7f5b2d6 100644
--- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
+++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
@@ -1,546 +1,636 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.zxing.integration.android.IntentIntegrator;
-import it.reyboz.bustorino.ActivityMain;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.ArrivalsFetcher;
import it.reyboz.bustorino.backend.FiveTAPIFetcher;
import it.reyboz.bustorino.backend.FiveTScraperFetcher;
import it.reyboz.bustorino.backend.FiveTStopsFetcher;
import it.reyboz.bustorino.backend.GTTJSONFetcher;
import it.reyboz.bustorino.backend.GTTStopsFetcher;
-import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsFinderByName;
-import it.reyboz.bustorino.data.UserDB;
import it.reyboz.bustorino.middleware.AsyncDataDownload;
import it.reyboz.bustorino.util.Permissions;
import static android.content.Context.LOCATION_SERVICE;
import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN;
/**
* A simple {@link Fragment} subclass.
* Use the {@link MainScreenFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{
- private final String OPTION_SHOW_LEGEND = "show_legend";
+ private static final String OPTION_SHOW_LEGEND = "show_legend";
+ private static final String SAVED_FRAGMENT="saved_fragment";
private static final String DEBUG_TAG = "BusTO - MainFragment";
public final static String FRAGMENT_TAG = "MainScreenFragment";
/// UI ELEMENTS //
private ImageButton addToFavorites;
private FragmentHelper fragmentHelper;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText busStopSearchByIDEditText;
private EditText busStopSearchByNameEditText;
private ProgressBar progressBar;
private TextView howDoesItWorkTextView;
private Button hideHintButton;
private MenuItem actionHelpMenuItem;
private FloatingActionButton floatingActionButton;
private boolean setupOnAttached = true;
+ private boolean suppressArrivalsReload = false;
//private Snackbar snackbar;
/*
* Search mode
*/
private static final int SEARCH_BY_NAME = 0;
private static final int SEARCH_BY_ID = 1;
private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12
private int searchMode;
//private ImageButton addToFavorites;
private final ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()};
//// HIDDEN BUT IMPORTANT ELEMENTS ////
FragmentManager fragMan;
Handler mainHandler;
private final Runnable refreshStop = new Runnable() {
public void run() {
if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame);
if (fragment == null){
//we create a new fragment, which is WRONG
new AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute();
} else{
String stopName = fragment.getStopID();
new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName);
}
} else //we create a new fragment, which is WRONG
new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute();
}
};
/// LOCATION STUFF ///
boolean pendingNearbyStopsRequest = false;
LocationManager locmgr;
private final Criteria cr = new Criteria();
//// ACTIVITY ATTACHED (LISTENER ///
private CommonFragmentListener mListener;
+ private String pendingStopID = null;
+
public MainScreenFragment() {
// Required empty public constructor
}
public static MainScreenFragment newInstance() {
MainScreenFragment fragment = new MainScreenFragment();
Bundle args = new Bundle();
//args.putString(ARG_PARAM1, param1);
//args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
//do nothing
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View root = inflater.inflate(R.layout.fragment_main_screen, container, false);
addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites);
busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText);
busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText);
progressBar = root.findViewById(R.id.progressBar);
howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView);
hideHintButton = root.findViewById(R.id.hideHintButton);
swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout);
floatingActionButton = root.findViewById(R.id.floatingActionButton);
busStopSearchByIDEditText.setSelectAllOnFocus(true);
busStopSearchByIDEditText
.setOnEditorActionListener((v, actionId, event) -> {
// IME_ACTION_SEARCH alphabetical option
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
onSearchClick(v);
return true;
}
return false;
});
busStopSearchByNameEditText
.setOnEditorActionListener((v, actionId, event) -> {
// IME_ACTION_SEARCH alphabetical option
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
onSearchClick(v);
return true;
}
return false;
});
swipeRefreshLayout
.setOnRefreshListener(() -> mainHandler.post(refreshStop));
swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500);
floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout));
hideHintButton.setOnClickListener(this::onHideHint);
AppCompatImageButton qrButton = root.findViewById(R.id.QRButton);
qrButton.setOnClickListener(this::onQRButtonClick);
AppCompatImageButton searchButton = root.findViewById(R.id.searchButton);
searchButton.setOnClickListener(this::onSearchClick);
// Fragment stuff
fragMan = getChildFragmentManager();
fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED"));
fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame);
setSearchModeBusStopID();
cr.setAccuracy(Criteria.ACCURACY_FINE);
cr.setAltitudeRequired(false);
cr.setBearingRequired(false);
cr.setCostAllowed(true);
cr.setPowerRequirement(Criteria.NO_REQUIREMENT);
locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE);
+
+ Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null));
+
+
+
return root;
}
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE));
+ Log.d(DEBUG_TAG, "Setup on attached: "+setupOnAttached);
+ //Restore instance state
+ if (savedInstanceState!=null){
+ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT);
+ if (fragment!=null){
+ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit();
+ setupOnAttached = false;
+ }
+ }
+ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){
+ swipeRefreshLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame);
+ if (fragment!=null)
+ getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment);
+ }
+
+ public void setSuppressArrivalsReload(boolean value){
+ suppressArrivalsReload = value;
+ // we have to suppress the reloading of the (possible) ArrivalsFragment
+ /*if(value) {
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame);
+ if (fragment instanceof ArrivalsFragment) {
+ ArrivalsFragment frag = (ArrivalsFragment) fragment;
+ frag.setReloadOnResume(false);
+ }
+ }
+
+ */
+ }
+
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
+ Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached);
mainHandler = new Handler();
if (context instanceof CommonFragmentListener) {
mListener = (CommonFragmentListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement CommonFragmentListener");
}
- if (setupOnAttached){
+ if (setupOnAttached) {
+ if (pendingStopID==null)
//We want the nearby bus stops!
- mainHandler.post(new NearbyStopsRequester(getContext(),cr, locListener));
+ mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener));
+ else{
+ ///TODO: if there is a stop displayed, we need to hold the update
+ }
//If there are no providers available, then, wait for them
setupOnAttached = false;
+ } else {
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
+ // setupOnAttached = true;
}
@Override
public void onResume() {
final Context con = getContext();
if (con != null)
locmgr = (LocationManager) getContext().getSystemService(LOCATION_SERVICE);
else {
Log.w(DEBUG_TAG, "Context is null at onResume");
}
super.onResume();
+ // if we have a pending stopID request, do it
+ Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID);
+ //this is the second time we are attaching this fragment
+ Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload);
+ if (suppressArrivalsReload){
+ // we have to suppress the reloading of the (possible) ArrivalsFragment
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame);
+ if (fragment instanceof ArrivalsFragment){
+ ArrivalsFragment frag = (ArrivalsFragment) fragment;
+ frag.setReloadOnResume(false);
+ }
+ suppressArrivalsReload = false;
+ }
+ if(pendingStopID!=null){
+ requestArrivalsForStopID(pendingStopID);
+ pendingStopID = null;
+ }
}
@Override
public void onPause() {
//mainHandler = null;
locmgr = null;
super.onPause();
}
/*
GUI METHODS
*/
/**
* QR scan button clicked
*
* @param v View QRButton clicked
*/
public void onQRButtonClick(View v) {
IntentIntegrator integrator = new IntentIntegrator(getActivity());
integrator.initiateScan();
}
public void onHideHint(View v) {
hideHints();
setOption(OPTION_SHOW_LEGEND, false);
}
/**
* OK this is pure shit
*
* @param v View clicked
*/
public void onSearchClick(View v) {
final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()};
if (searchMode == SEARCH_BY_ID) {
String busStopID = busStopSearchByIDEditText.getText().toString();
requestArrivalsForStopID(busStopID);
} else { // searchMode == SEARCH_BY_NAME
String query = busStopSearchByNameEditText.getText().toString();
//new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper);
new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query);
}
}
public void onToggleKeyboardLayout(View v) {
if (searchMode == SEARCH_BY_NAME) {
setSearchModeBusStopID();
if (busStopSearchByIDEditText.requestFocus()) {
showKeyboard();
}
} else { // searchMode == SEARCH_BY_ID
setSearchModeBusStopName();
if (busStopSearchByNameEditText.requestFocus()) {
showKeyboard();
}
}
}
@Override
public void enableRefreshLayout(boolean yes) {
swipeRefreshLayout.setEnabled(yes);
}
////////////////////////////////////// GUI HELPERS /////////////////////////////////////////////
public void showKeyboard() {
+ if (getActivity() == null)
+ return;
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText;
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
private void setSearchModeBusStopID() {
searchMode = SEARCH_BY_ID;
busStopSearchByNameEditText.setVisibility(View.GONE);
busStopSearchByNameEditText.setText("");
busStopSearchByIDEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.alphabetical);
}
private void setSearchModeBusStopName() {
searchMode = SEARCH_BY_NAME;
busStopSearchByIDEditText.setVisibility(View.GONE);
busStopSearchByIDEditText.setText("");
busStopSearchByNameEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.numeric);
}
/**
* Having that cursor at the left of the edit text makes me cancer.
*
* @param busStopID bus stop ID
*/
private void setBusStopSearchByIDEditText(String busStopID) {
busStopSearchByIDEditText.setText(busStopID);
busStopSearchByIDEditText.setSelection(busStopID.length());
}
private void showHints() {
howDoesItWorkTextView.setVisibility(View.VISIBLE);
hideHintButton.setVisibility(View.VISIBLE);
//actionHelpMenuItem.setVisible(false);
}
private void hideHints() {
howDoesItWorkTextView.setVisibility(View.GONE);
hideHintButton.setVisibility(View.GONE);
//actionHelpMenuItem.setVisible(true);
}
@Override
public void toggleSpinner(boolean enable) {
if (enable) {
//already set by the RefreshListener when needed
//swipeRefreshLayout.setRefreshing(true);
progressBar.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
}
}
private void prepareGUIForBusLines() {
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setVisibility(View.VISIBLE);
//actionHelpMenuItem.setVisible(true);
}
private void prepareGUIForBusStops() {
swipeRefreshLayout.setEnabled(false);
swipeRefreshLayout.setVisibility(View.VISIBLE);
//actionHelpMenuItem.setVisible(false);
}
@Override
public void showFloatingActionButton(boolean yes) {
mListener.showFloatingActionButton(yes);
}
/**
* This provides a temporary fix to make the transition
* to a single asynctask go smoother
*
* @param fragmentType the type of fragment created
*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
hideKeyboard();
//if we are getting results, already, stop waiting for nearbyStops
if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) {
locmgr.removeUpdates(locListener);
pendingNearbyStopsRequest = false;
}
if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType");
else
switch (fragmentType) {
case ARRIVALS:
prepareGUIForBusLines();
if (getOption(OPTION_SHOW_LEGEND, true)) {
showHints();
}
break;
case STOPS:
prepareGUIForBusStops();
break;
default:
Log.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment");
return;
}
// Shows hints
}
+ /**
+ * Main method for stops requests
+ * @param ID the Stop ID
+ */
@Override
public void requestArrivalsForStopID(String ID) {
+ if (!isResumed()){
+ //defer request
+ pendingStopID = ID;
+ Log.d(DEBUG_TAG, "Deferring update for stop "+ID);
+ return;
+ }
+ final boolean delayedRequest = !(pendingStopID==null);
final FragmentManager framan = getChildFragmentManager();
+ if (getContext()==null){
+ Log.e(DEBUG_TAG, "Asked for arrivals with null context");
+ return;
+ }
if (ID == null || ID.length() <= 0) {
// we're still in UI thread, no need to mess with Progress
showToastMessage(R.string.insert_bus_stop_number_error, true);
toggleSpinner(false);
} else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame);
if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){
// Run with previous fetchers
//fragment.getCurrentFetchers().toArray()
new AsyncDataDownload(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID);
} else{
new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID);
}
}
else {
new AsyncDataDownload(fragmentHelper,arrivalsFetchers, getContext()).execute(ID);
- Log.d("MainActiv", "Started search for arrivals of stop " + ID);
+ Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID);
}
}
/////////// LOCATION METHODS //////////
final LocationListener locListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
Log.d(DEBUG_TAG, "Location changed");
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d(DEBUG_TAG, "Location provider status: " + status);
if (status == LocationProvider.AVAILABLE) {
resolveStopRequest(provider);
}
}
@Override
public void onProviderEnabled(String provider) {
resolveStopRequest(provider);
}
@Override
public void onProviderDisabled(String provider) {
}
};
private void resolveStopRequest(String provider) {
Log.d(DEBUG_TAG, "Provider " + provider + " got enabled");
if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) {
pendingNearbyStopsRequest = false;
mainHandler.post(new NearbyStopsRequester(getContext(), cr, locListener));
}
}
/**
* Run location requests separately and asynchronously
*/
class NearbyStopsRequester implements Runnable {
Context appContext;
Criteria cr;
LocationListener listener;
public NearbyStopsRequester(Context appContext, Criteria criteria, LocationListener listener) {
this.appContext = appContext.getApplicationContext();
this.cr = criteria;
this.listener = listener;
}
@Override
public void run() {
final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false);
final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED;
//if we don't have the permission, we have to ask for it, if we haven't
// asked too many times before
if (noPermission) {
if (!canRunPosition) {
pendingNearbyStopsRequest = true;
Permissions.assertLocationPermissions(appContext,getActivity());
Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission);
return;
} else {
Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show();
}
} else setOption(LOCATION_PERMISSION_GIVEN, true);
LocationManager locManager = (LocationManager) appContext.getSystemService(LOCATION_SERVICE);
if (locManager == null) {
Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment");
return;
}
if (Permissions.anyLocationProviderMatchesCriteria(locManager, cr, true)
&& fragmentHelper.getLastSuccessfullySearchedBusStop() == null
&& !fragMan.isDestroyed()) {
//Go ahead with the request
Log.d("mainActivity", "Recreating stop fragment");
swipeRefreshLayout.setVisibility(View.VISIBLE);
NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS);
Fragment oldFrag = fragMan.findFragmentById(R.id.resultFrame);
FragmentTransaction ft = fragMan.beginTransaction();
if (oldFrag != null)
ft.remove(oldFrag);
ft.add(R.id.resultFrame, fragment, "nearbyStop_correct");
ft.commit();
//fragMan.executePendingTransactions();
pendingNearbyStopsRequest = false;
} else if (!Permissions.anyLocationProviderMatchesCriteria(locManager, cr, true)) {
//Wait for the providers
Log.d(DEBUG_TAG, "Queuing position request");
pendingNearbyStopsRequest = true;
locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, listener);
}
}
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
index 68981b0..0d68be7 100644
--- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
@@ -1,652 +1,654 @@
/*
BusTO - Fragments components
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.core.util.Pair;
import androidx.preference.PreferenceManager;
import androidx.appcompat.widget.AppCompatButton;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.volley.*;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.ArrivalsStopAdapter;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType;
import it.reyboz.bustorino.middleware.AppLocationManager;
import it.reyboz.bustorino.data.AppDataProvider;
import it.reyboz.bustorino.data.NextGenDB.Contract.*;
import it.reyboz.bustorino.adapters.SquareStopAdapter;
import it.reyboz.bustorino.util.LocationCriteria;
import it.reyboz.bustorino.util.StopSorterByDistance;
import java.util.*;
public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks {
private FragmentListenerMain mListener;
private FragmentLocationListener fragmentLocationListener;
private final String[] PROJECTION = {StopsTable.COL_ID,StopsTable.COL_LAT,StopsTable.COL_LONG,
StopsTable.COL_NAME,StopsTable.COL_TYPE,StopsTable.COL_LINES_STOPPING};
private final static String DEBUG_TAG = "NearbyStopsFragment";
private final static String FRAGMENT_TYPE_KEY = "FragmentType";
public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20;
private int fragment_type;
//data Bundle
private final String BUNDLE_LOCATION = "location";
private final int LOADER_ID = 0;
private RecyclerView gridRecyclerView;
private SquareStopAdapter dataAdapter;
private AutoFitGridLayoutManager gridLayoutManager;
boolean canStartDBQuery = true;
private Location lastReceivedLocation = null;
private ProgressBar circlingProgressBar,flatProgressBar;
private int distance;
protected SharedPreferences globalSharedPref;
private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener;
private TextView messageTextView,titleTextView;
private CommonScrollListener scrollListener;
private AppCompatButton switchButton;
private boolean firstLocForStops = true,firstLocForArrivals = true;
public static final int COLUMN_WIDTH_DP = 250;
private Integer MAX_DISTANCE = -3;
private int MIN_NUM_STOPS = -1;
private int TIME_INTERVAL_REQUESTS = -1;
private AppLocationManager locManager;
//These are useful for the case of nearby arrivals
private ArrivalsManager arrivalsManager = null;
private ArrivalsStopAdapter arrivalsStopAdapter = null;
public NearbyStopsFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
* @return A new instance of fragment NearbyStopsFragment.
*/
public static NearbyStopsFragment newInstance(int fragmentType) {
if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS )
throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED");
NearbyStopsFragment fragment = new NearbyStopsFragment();
final Bundle args = new Bundle(1);
args.putInt(FRAGMENT_TYPE_KEY,fragmentType);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
setFragmentType(getArguments().getInt(FRAGMENT_TYPE_KEY));
}
locManager = AppLocationManager.getInstance(getContext());
fragmentLocationListener = new FragmentLocationListener(this);
globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences),Context.MODE_PRIVATE);
globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false);
gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView);
gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP));
gridRecyclerView.setLayoutManager(gridLayoutManager);
gridRecyclerView.setHasFixedSize(false);
circlingProgressBar = root.findViewById(R.id.loadingBar);
flatProgressBar = root.findViewById(R.id.horizontalProgressBar);
messageTextView = root.findViewById(R.id.messageTextView);
titleTextView = root.findViewById(R.id.titleTextView);
switchButton = root.findViewById(R.id.switchButton);
preferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Log.d(DEBUG_TAG,"Key "+key+" was changed");
if(key.equals(getString(R.string.databaseUpdatingPref))){
if(!sharedPreferences.getBoolean(getString(R.string.databaseUpdatingPref),true)){
canStartDBQuery = true;
Log.d(DEBUG_TAG,"The database has finished updating, can start update now");
}
}
}
};
scrollListener = new CommonScrollListener(mListener,false);
switchButton.setOnClickListener(v -> {
switchFragmentType();
});
+ Log.d(DEBUG_TAG, "onCreateView");
return root;
}
protected ArrayList createStopListFromCursor(Cursor data){
ArrayList stopList = new ArrayList<>();
final int col_id = data.getColumnIndex(StopsTable.COL_ID);
final int latInd = data.getColumnIndex(StopsTable.COL_LAT);
final int lonInd = data.getColumnIndex(StopsTable.COL_LONG);
final int nameindex = data.getColumnIndex(StopsTable.COL_NAME);
final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE);
final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING);
data.moveToFirst();
for(int i=0; i onCreateLoader(int id, Bundle args) {
//BUILD URI
lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION);
Uri.Builder builder = new Uri.Builder();
builder.scheme("content").authority(AppDataProvider.AUTHORITY)
.appendPath("stops").appendPath("location")
.appendPath(String.valueOf(lastReceivedLocation.getLatitude()))
.appendPath(String.valueOf(lastReceivedLocation.getLongitude()))
.appendPath(String.valueOf(distance)); //distance
CursorLoader cl = new CursorLoader(getContext(),builder.build(),PROJECTION,null,null,null);
cl.setUpdateThrottle(2000);
return cl;
}
@Override
public void onLoadFinished(@NonNull Loader loader, Cursor data) {
if (0 > MAX_DISTANCE) throw new AssertionError();
//Cursor might be null
if(data==null){
Log.e(DEBUG_TAG,"Null cursor, something really wrong happened");
return;
}
if(!isDBUpdating() && (data.getCount() stopList = createStopListFromCursor(data);
if(data.getCount()>0) {
//quick trial to hopefully always get the stops in the correct order
Collections.sort(stopList,new StopSorterByDistance(lastReceivedLocation));
switch (fragment_type){
case TYPE_STOPS:
showStopsInRecycler(stopList);
break;
case TYPE_ARRIVALS:
arrivalsManager = new ArrivalsManager(stopList);
flatProgressBar.setVisibility(View.VISIBLE);
flatProgressBar.setProgress(0);
flatProgressBar.setIndeterminate(false);
//for the moment, be satisfied with only one location
//AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener);
break;
default:
}
} else {
setNoStopsLayout();
}
}
@Override
public void onLoaderReset(Loader loader) {
}
/**
* To enable targeting from the Button
*/
public void switchFragmentType(View v){
switchFragmentType();
}
/**
* Call when you need to switch the type of fragment
*/
private void switchFragmentType(){
if(fragment_type==TYPE_ARRIVALS){
setFragmentType(TYPE_STOPS);
switchButton.setText(getString(R.string.show_arrivals));
titleTextView.setText(getString(R.string.nearby_stops_message));
if(arrivalsManager!=null)
arrivalsManager.cancelAllRequests();
if(dataAdapter!=null)
gridRecyclerView.setAdapter(dataAdapter);
} else if (fragment_type==TYPE_STOPS){
setFragmentType(TYPE_ARRIVALS);
titleTextView.setText(getString(R.string.nearby_arrivals_message));
switchButton.setText(getString(R.string.show_stops));
if(arrivalsStopAdapter!=null)
gridRecyclerView.setAdapter(arrivalsStopAdapter);
}
fragmentLocationListener.lastUpdateTime = -1;
locManager.removeLocationRequestFor(fragmentLocationListener);
locManager.addLocationRequestFor(fragmentLocationListener);
}
//useful methods
protected boolean isDBUpdating(){
return globalSharedPref.getBoolean(getString(R.string.databaseUpdatingPref),false);
}
/////// GUI METHODS ////////
private void showStopsInRecycler(List stops){
if(firstLocForStops) {
dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation);
gridRecyclerView.setAdapter(dataAdapter);
firstLocForStops = false;
}else {
dataAdapter.setStops(stops);
dataAdapter.setUserPosition(lastReceivedLocation);
}
dataAdapter.notifyDataSetChanged();
//showRecyclerHidingLoadMessage();
if (gridRecyclerView.getVisibility() != View.VISIBLE) {
circlingProgressBar.setVisibility(View.GONE);
gridRecyclerView.setVisibility(View.VISIBLE);
}
messageTextView.setVisibility(View.GONE);
}
private void showArrivalsInRecycler(List palinas){
Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation));
final ArrayList> routesPairList = new ArrayList<>(10);
//int maxNum = Math.min(MAX_STOPS, stopList.size());
for(Palina p: palinas){
//if there are no routes available, skip stop
if(p.queryAllRoutes().size() == 0) continue;
for(Route r: p.queryAllRoutes()){
//if there are no routes, should not do anything
routesPairList.add(new Pair<>(p,r));
}
}
if(firstLocForArrivals){
arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation);
gridRecyclerView.setAdapter(arrivalsStopAdapter);
firstLocForArrivals = false;
} else {
arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation);
}
//arrivalsStopAdapter.notifyDataSetChanged();
showRecyclerHidingLoadMessage();
}
private void setNoStopsLayout(){
messageTextView.setVisibility(View.VISIBLE);
messageTextView.setText(R.string.no_stops_nearby);
circlingProgressBar.setVisibility(View.GONE);
}
/**
* Does exactly what is says on the tin
*/
private void showRecyclerHidingLoadMessage(){
if (gridRecyclerView.getVisibility() != View.VISIBLE) {
circlingProgressBar.setVisibility(View.GONE);
gridRecyclerView.setVisibility(View.VISIBLE);
}
messageTextView.setVisibility(View.GONE);
}
class ArrivalsManager implements FiveTAPIVolleyRequest.ResponseListener, Response.ErrorListener{
final HashMap mStops;
final Map> routesToAdd = new HashMap<>();
final static String REQUEST_TAG = "NearbyArrivals";
private final QueryType[] types = {QueryType.ARRIVALS,QueryType.DETAILS};
final NetworkVolleyManager volleyManager;
private final int MAX_ARRIVAL_STOPS =35;
int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0;
ArrivalsManager(List stops){
mStops = new HashMap<>();
volleyManager = NetworkVolleyManager.getInstance(getContext());
for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){
mStops.put(s.ID,new Palina(s));
for(QueryType t: types) {
final FiveTAPIVolleyRequest req = FiveTAPIVolleyRequest.getNewRequest(t, s.ID, this, this);
if (req != null) {
req.setTag(REQUEST_TAG);
volleyManager.addToRequestQueue(req);
activeRequestCount++;
}
}
}
flatProgressBar.setMax(activeRequestCount);
}
@Override
public void onErrorResponse(VolleyError error) {
if(error instanceof ParseError){
//TODO
Log.w(DEBUG_TAG,"Parsing error for stop request");
} else if (error instanceof NetworkError){
String s;
if(error.networkResponse!=null)
s = new String(error.networkResponse.data);
else s="";
Log.w(DEBUG_TAG,"Network error: "+s);
}else {
Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage());
}
if(error.networkResponse!=null){
Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode);
}
//counters
activeRequestCount--;
reqErrorCount++;
flatProgressBar.setProgress(reqErrorCount+reqSuccessCount);
}
@Override
public void onResponse(Palina result, QueryType type) {
//counter for requests
activeRequestCount--;
reqSuccessCount++;
final Palina palinaInMap = mStops.get(result.ID);
//palina cannot be null here
//sorry for the brutal crash when it happens
if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map");
//necessary to split the Arrivals and Details cases
switch (type){
case ARRIVALS:
palinaInMap.addInfoFromRoutes(result.queryAllRoutes());
final List possibleRoutes = routesToAdd.get(result.ID);
if(possibleRoutes!=null) {
palinaInMap.addInfoFromRoutes(possibleRoutes);
routesToAdd.remove(result.ID);
}
break;
case DETAILS:
if(palinaInMap.queryAllRoutes().size()>0){
//merge the branches
palinaInMap.addInfoFromRoutes(result.queryAllRoutes());
} else {
routesToAdd.put(result.ID,result.queryAllRoutes());
}
break;
default:
throw new IllegalArgumentException("Wrong QueryType in onResponse");
}
final ArrayList outList = new ArrayList<>();
for(Palina p: mStops.values()){
final List routes = p.queryAllRoutes();
if(routes!=null && routes.size()>0) outList.add(p);
}
showArrivalsInRecycler(outList);
flatProgressBar.setProgress(reqErrorCount+reqSuccessCount);
if(activeRequestCount==0) {
flatProgressBar.setIndeterminate(true);
flatProgressBar.setVisibility(View.GONE);
}
}
void cancelAllRequests(){
volleyManager.getRequestQueue().cancelAll(REQUEST_TAG);
flatProgressBar.setVisibility(View.GONE);
}
}
/**
* Local locationListener, to use for the GPS
*/
class FragmentLocationListener implements AppLocationManager.LocationRequester{
LoaderManager.LoaderCallbacks callbacks;
private int oldLocStatus = -2;
private LocationCriteria cr;
private long lastUpdateTime = -1;
public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) {
this.callbacks = callbacks;
}
@Override
public void onLocationChanged(Location location) {
//set adapter
float accuracy = location.getAccuracy();
if(accuracy<60 && canStartDBQuery) {
distance = 20;
final Bundle msgBundle = new Bundle();
msgBundle.putParcelable(BUNDLE_LOCATION,location);
getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks);
}
lastUpdateTime = System.currentTimeMillis();
Log.d("BusTO:NearPositListen","can start loader "+ canStartDBQuery);
}
@Override
public void onLocationStatusChanged(int status) {
switch(status){
case AppLocationManager.LOCATION_GPS_AVAILABLE:
messageTextView.setVisibility(View.GONE);
break;
case AppLocationManager.LOCATION_UNAVAILABLE:
messageTextView.setText(R.string.enableGpsText);
messageTextView.setVisibility(View.VISIBLE);
break;
default:
Log.e(DEBUG_TAG,"Location status not recognized");
}
}
@Override
public LocationCriteria getLocationCriteria() {
return new LocationCriteria(60,TIME_INTERVAL_REQUESTS);
}
@Override
public long getLastUpdateTimeMillis() {
return lastUpdateTime;
}
void resetUpdateTime(){
lastUpdateTime = -1;
}
}
/**
* Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example)
*
*/
class AutoFitGridLayoutManager extends GridLayoutManager {
private int columnWidth;
private boolean columnWidthChanged = true;
public AutoFitGridLayoutManager(Context context, int columnWidth) {
super(context, 1);
setColumnWidth(columnWidth);
}
public void setColumnWidth(int newColumnWidth) {
if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
columnWidth = newColumnWidth;
columnWidthChanged = true;
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (columnWidthChanged && columnWidth > 0) {
int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = getHeight() - getPaddingTop() - getPaddingBottom();
}
int spanCount = Math.max(1, totalSpace / columnWidth);
setSpanCount(spanCount);
columnWidthChanged = false;
}
super.onLayoutChildren(recycler, state);
}
}
}
diff --git a/src/it/reyboz/bustorino/fragments/ResultListFragment.java b/src/it/reyboz/bustorino/fragments/ResultListFragment.java
index edf4ea9..24146ac 100644
--- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java
+++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java
@@ -1,298 +1,298 @@
/*
BusTO - Fragments components
Copyright (C) 2016 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.Parcelable;
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 com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.UserDB;
/**
* This is a generalized fragment that can be used both for
*
*
*/
public class ResultListFragment extends Fragment{
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
static final String LIST_TYPE = "list-type";
protected static final String LIST_STATE = "list_state";
protected static final String MESSAGE_TEXT_VIEW = "message_text_view";
private FragmentKind adapterKind;
private boolean adapterSet = false;
protected FragmentListenerMain mListener;
protected TextView messageTextView;
protected ListView resultsListView;
private FloatingActionButton fabutton;
private ListAdapter mListAdapter = null;
boolean listShown;
private Parcelable mListInstanceState = null;
public ResultListFragment() {
// Required empty public constructor
}
public ListView getResultsListView() {
return resultsListView;
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param listType whether the list is used for STOPS or LINES (Orari)
* @return A new instance of fragment ResultListFragment.
*/
public static ResultListFragment newInstance(FragmentKind listType, String eventualStopTitle) {
ResultListFragment fragment = new ResultListFragment();
Bundle args = new Bundle();
args.putSerializable(LIST_TYPE, listType);
if (eventualStopTitle != null) {
args.putString(ArrivalsFragment.STOP_TITLE, eventualStopTitle);
}
fragment.setArguments(args);
return fragment;
}
public static ResultListFragment newInstance(FragmentKind listType) {
return newInstance(listType, null);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
adapterKind = (FragmentKind) getArguments().getSerializable(LIST_TYPE);
}
}
/**
* Check if the last Bus Stop is in the favorites
* @return true if it iss
*/
public boolean isStopInFavorites(String busStopId) {
boolean found = false;
// no stop no party
if(busStopId != null) {
SQLiteDatabase userDB = new UserDB(getContext()).getReadableDatabase();
found = UserDB.isStopInFavorites(userDB, busStopId);
}
return found;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_list_view, container, false);
messageTextView = (TextView) root.findViewById(R.id.messageTextView);
if (adapterKind != null) {
resultsListView = (ListView) root.findViewById(R.id.resultsListView);
switch (adapterKind) {
case STOPS:
resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
/*
* Casting because of Javamerda
* @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener
*/
Stop busStop = (Stop) parent.getItemAtPosition(position);
mListener.requestArrivalsForStopID(busStop.ID);
}
});
// set the textviewMessage
setTextViewMessage(getString(R.string.results));
break;
case ARRIVALS:
resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long 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(ArrivalsFragment.STOP_TITLE);
setTextViewMessage(String.format(
getString(R.string.passages), displayName));
break;
default:
throw new IllegalStateException("Argument passed was not of a supported type");
}
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);
}
} else
Log.d(getString(R.string.list_fragment_debug), "No content root for fragment");
return root;
}
public boolean isFragmentForTheSameStop(Palina p) {
return adapterKind.equals(FragmentKind.ARRIVALS) && getTag().equals(getFragmentTag(p));
}
public static String getFragmentTag(Palina p) {
- return p.ID;
+ return "palina_"+p.ID;
}
@Override
public void onResume() {
super.onResume();
//Log.d(getString(R.string.list_fragment_debug),"Fragment restored, saved listAdapter is "+(mListAdapter));
if (mListAdapter != null) {
ListAdapter adapter = mListAdapter;
mListAdapter = null;
resetListAdapter(adapter);
}
if (mListInstanceState != null) {
Log.d("resultsListView", "trying to restore instance state");
resultsListView.onRestoreInstanceState(mListInstanceState);
}
switch (adapterKind) {
case ARRIVALS:
resultsListView.setOnScrollListener(new CommonScrollListener(mListener, true));
fabutton.show();
break;
case STOPS:
resultsListView.setOnScrollListener(new CommonScrollListener(mListener, false));
break;
default:
//NONE
}
mListener.readyGUIfor(adapterKind);
}
@Override
public void onPause() {
if (adapterKind.equals(FragmentKind.ARRIVALS)) {
SwipeRefreshLayout reflay = getActivity().findViewById(R.id.listRefreshLayout);
reflay.setEnabled(false);
Log.d("BusTO Fragment " + this.getTag(), "RefreshLayout disabled");
}
super.onPause();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof FragmentListenerMain) {
mListener = (FragmentListenerMain) context;
fabutton = (FloatingActionButton) getActivity().findViewById(R.id.floatingActionButton);
} else {
throw new RuntimeException(context.toString()
+ " must implement ResultFragmentListener");
}
}
@Override
public void onDetach() {
mListener = null;
if (fabutton != null)
fabutton.show();
super.onDetach();
}
@Override
public void onDestroyView() {
resultsListView = null;
//Log.d(getString(R.string.list_fragment_debug), "called onDestroyView");
getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString());
super.onDestroyView();
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
Log.d("ResultListFragment", "onViewStateRestored");
if (savedInstanceState != null) {
mListInstanceState = savedInstanceState.getParcelable(LIST_STATE);
Log.d("ResultListFragment", "listInstanceStatePresent :" + mListInstanceState);
}
}
protected void resetListAdapter(ListAdapter adapter) {
boolean hadAdapter = mListAdapter != null;
mListAdapter = adapter;
if (resultsListView != null) {
resultsListView.setAdapter(adapter);
resultsListView.setVisibility(View.VISIBLE);
}
}
public void setNewListAdapter(ListAdapter adapter){
resetListAdapter(adapter);
}
/**
* Set the message textView
* @param message the whole message to write in the textView
*/
public void setTextViewMessage(String message) {
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
index 16203c2..047e62e 100644
--- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
+++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
@@ -1,327 +1,329 @@
/*
BusTO (middleware)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.middleware;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.SQLException;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import android.util.Log;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.data.AppDataProvider;
import it.reyboz.bustorino.data.NextGenDB;
import it.reyboz.bustorino.fragments.FragmentHelper;
import it.reyboz.bustorino.data.NextGenDB.Contract.*;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Calendar;
/**
* This should be used to download data, but not to display it
*/
public class AsyncDataDownload extends AsyncTask{
private static final String TAG = "BusTO-DataDownload";
private boolean failedAll = false;
private final AtomicReference res;
private final RequestType t;
private String query;
WeakReference helperRef;
private final ArrayList otherActivities = new ArrayList<>();
private final Fetcher[] theFetchers;
private Context context;
+ private final boolean replaceFragment;
public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) {
RequestType type;
helperRef = new WeakReference<>(fh);
fh.setLastTaskRef(new WeakReference<>(this));
res = new AtomicReference<>();
this.context = context.getApplicationContext();
+ this.replaceFragment = true;
theFetchers = fetchers;
if (theFetchers.length < 1){
throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!");
}
if (theFetchers[0] instanceof ArrivalsFetcher){
type = RequestType.ARRIVALS;
} else if (theFetchers[0] instanceof StopsFinderByName){
type = RequestType.STOPS;
} else{
type = null;
}
t = type;
}
@Override
protected Object doInBackground(String... params) {
RecursionHelper r = new RecursionHelper<>(theFetchers);
boolean success=false;
Object result;
FragmentHelper fh = helperRef.get();
//If the FragmentHelper is null, that means the activity doesn't exist anymore
if (fh == null){
return null;
}
//Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue());
while(r.valid()) {
if(this.isCancelled()) {
return null;
}
//get the data from the fetcher
switch (t){
case ARRIVALS:
ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward();
Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass());
Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop();
Palina p;
String stopID;
if(params.length>0)
stopID=params[0]; //(it's a Palina)
else if(lastSearchedBusStop!=null)
stopID = lastSearchedBusStop.ID; //(it's a Palina)
else {
publishProgress(Fetcher.result.QUERY_TOO_SHORT);
return null;
}
//Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times
if(f instanceof FiveTAPIFetcher && Integer.parseInt(stopID)>= 8200)
continue;
p= f.ReadArrivalTimesAll(stopID,res);
publishProgress(res.get());
if(f instanceof FiveTAPIFetcher){
AtomicReference gres = new AtomicReference<>();
List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres);
if(gres.get() == Fetcher.result.OK){
p.addInfoFromRoutes(branches);
Thread t = new Thread(new BranchInserter(branches, context));
t.start();
otherActivities.add(t);
}
//put updated values into Database
}
if(lastSearchedBusStop != null && res.get()== Fetcher.result.OK) {
// check that we don't have the same stop
if(lastSearchedBusStop.ID.equals(p.ID)) {
// searched and it's the same
String sn = lastSearchedBusStop.getStopDisplayName();
if(sn != null) {
// "merge" Stop over Palina and we're good to go
p.mergeNameFrom(lastSearchedBusStop);
}
}
}
result = p;
//TODO: find a way to avoid overloading the user with toasts
break;
case STOPS:
StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward();
List resultList= finder.FindByName(params[0], this.res); //it's a List
Log.d(TAG,"Using the StopFinderByName: "+finder.getClass());
query =params[0];
result = resultList; //dummy result
break;
default:
result = null;
}
//find if it went well
if(res.get()== Fetcher.result.OK) {
//wait for other threads to finish
for(Thread t: otherActivities){
try {
t.join();
} catch (InterruptedException e) {
//do nothing
}
}
return result;
}
}
//at this point, we are sure that the result has been negative
failedAll=true;
return null;
}
@Override
protected void onProgressUpdate(Fetcher.result... values) {
FragmentHelper fh = helperRef.get();
if (fh!=null)
for (Fetcher.result r : values){
//TODO: make Toast
fh.showErrorMessage(r);
}
else {
Log.w(TAG,"We had to show some progress but activity was destroyed");
}
}
@Override
protected void onPostExecute(Object o) {
FragmentHelper fh = helperRef.get();
if(failedAll || o == null || fh == null){
//everything went bad
if(fh!=null) fh.toggleSpinner(false);
cancel(true);
//TODO: send message here
return;
}
if(isCancelled()) return;
switch (t){
case ARRIVALS:
Palina palina = (Palina) o;
- fh.createOrUpdateStopFragment(palina);
+ fh.createOrUpdateStopFragment(palina, replaceFragment);
break;
case STOPS:
//this should never be a problem
List stopList = (List) o;
if(query!=null && !isCancelled()) {
- fh.createFragmentFor(stopList,query);
+ fh.createStopListFragment(stopList,query, replaceFragment);
} else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT");
break;
case DBUPDATE:
break;
}
}
@Override
protected void onCancelled() {
FragmentHelper fh = helperRef.get();
if (fh!=null) fh.toggleSpinner(false);
}
@Override
protected void onPreExecute() {
FragmentHelper fh = helperRef.get();
if (fh!=null) fh.toggleSpinner(true);
}
public enum RequestType {
ARRIVALS,STOPS,DBUPDATE
}
public class BranchInserter implements Runnable{
private final List routesToInsert;
private final Context context;
private final NextGenDB nextGenDB;
public BranchInserter(List routesToInsert,@NonNull Context con) {
this.routesToInsert = routesToInsert;
this.context = con;
nextGenDB = new NextGenDB(context);
}
@Override
public void run() {
ContentValues[] values = new ContentValues[routesToInsert.size()];
ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4);
long starttime,endtime;
for (Route r:routesToInsert){
//if it has received an interrupt, stop
if(Thread.interrupted()) return;
//otherwise, build contentValues
final ContentValues cv = new ContentValues();
cv.put(BranchesTable.COL_BRANCHID,r.branchid);
cv.put(LinesTable.COLUMN_NAME,r.getName());
cv.put(BranchesTable.COL_DIRECTION,r.destinazione);
cv.put(BranchesTable.COL_DESCRIPTION,r.description);
for (int day :r.serviceDays) {
switch (day){
case Calendar.MONDAY:
cv.put(BranchesTable.COL_LUN,1);
break;
case Calendar.TUESDAY:
cv.put(BranchesTable.COL_MAR,1);
break;
case Calendar.WEDNESDAY:
cv.put(BranchesTable.COL_MER,1);
break;
case Calendar.THURSDAY:
cv.put(BranchesTable.COL_GIO,1);
break;
case Calendar.FRIDAY:
cv.put(BranchesTable.COL_VEN,1);
break;
case Calendar.SATURDAY:
cv.put(BranchesTable.COL_SAB,1);
break;
case Calendar.SUNDAY:
cv.put(BranchesTable.COL_DOM,1);
break;
}
}
if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode());
cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode());
values[routesToInsert.indexOf(r)] = cv;
for(int i=0; i.
*/
package it.reyboz.bustorino.middleware;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.os.AsyncTask;
import android.widget.Toast;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Stop;
+import it.reyboz.bustorino.data.AppDataProvider;
import it.reyboz.bustorino.data.UserDB;
/**
* Handler to add or remove or toggle a Stop in your favorites
*/
public class AsyncStopFavoriteAction extends AsyncTask {
private final Context context;
+ private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
+ AppDataProvider.FAVORITES).build();
/**
* Kind of actions available
*/
- public enum Action { ADD, REMOVE, TOGGLE };
+ public enum Action { ADD, REMOVE, TOGGLE , UPDATE};
/**
* Action chosen
*
* Note that TOGGLE is not converted to ADD or REMOVE.
*/
private Action action;
// extra stuff to do after we've done it
private ResultListener listener;
/**
* Constructor
*
* @param context
* @param action
*/
public AsyncStopFavoriteAction(Context context, Action action, ResultListener listener) {
this.context = context.getApplicationContext();
this.action = action;
this.listener = listener;
}
@Override
protected Boolean doInBackground(Stop... stops) {
boolean result = false;
Stop stop = stops[0];
// check if the request has sense
if(stop != null) {
// get a writable database
UserDB userDatabase = new UserDB(context);
SQLiteDatabase db = userDatabase.getWritableDatabase();
// eventually toggle the status
if(Action.TOGGLE.equals(action)) {
if(UserDB.isStopInFavorites(db, stop.ID)) {
action = Action.REMOVE;
} else {
action = Action.ADD;
}
}
// at this point the action is just ADD or REMOVE
// add or remove?
if(Action.ADD.equals(action)) {
// add
result = UserDB.addOrUpdateStop(stop, db);
+ } else if (Action.UPDATE.equals(action)){
+
+ result = UserDB.updateStop(stop, db);
} else {
// remove
result = UserDB.deleteStop(stop, db);
}
// please sir, close the door
db.close();
}
return result;
}
/**
* Callback fired when everything was done
*
* @param result
*/
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if(result) {
+ UserDB.notifyContentProvider(context);
// at this point the action should be just ADD or REMOVE
if(Action.ADD.equals(action)) {
// now added
Toast.makeText(this.context, R.string.added_in_favorites, Toast.LENGTH_SHORT).show();
- } else {
+ } else if (Action.REMOVE.equals(action)) {
// now removed
Toast.makeText(this.context, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show();
}
} else {
// wtf
Toast.makeText(this.context, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show();
}
listener.doStuffWithResult(result);
}
public interface ResultListener{
/**
* Do what you need to to update the UI with the result
* @param result true if the action is done
*/
void doStuffWithResult(Boolean result);
}
}