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;
- /*
- * Database Access
- */
- private StopsDB stopsDB;
- private UserDB userDB;
private FragmentHelper fh;
///////////////////////////////// EVENT HANDLERS ///////////////////////////////////////////////
/*
* @see swipeRefreshLayout
*/
- private Handler handler = new Handler();
+ private final Handler theHandler = new Handler();
private final Runnable refreshing = new Runnable() {
public void run() {
if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame);
String stopName = fragment.getStopID();
new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray()).execute(stopName);
} else //we create a new fragment, which is WRONG
new AsyncDataDownload(fh, arrivalsFetchers).execute();
}
};
+
//// MAIN METHOD ///
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
framan = getSupportFragmentManager();
- this.stopsDB = new StopsDB(getApplicationContext());
- this.userDB = new UserDB(getApplicationContext());
+ final SharedPreferences theShPr = getMainSharedPreferences();
+ /*
+ * Database Access
+ */
setContentView(R.layout.activity_main);
- busStopSearchByIDEditText = (EditText) findViewById(R.id.busStopSearchByIDEditText);
- busStopSearchByNameEditText = (EditText) findViewById(R.id.busStopSearchByNameEditText);
- progressBar = (ProgressBar) findViewById(R.id.progressBar);
- howDoesItWorkTextView = (TextView) findViewById(R.id.howDoesItWorkTextView);
- hideHintButton = (Button) findViewById(R.id.hideHintButton);
- swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.listRefreshLayout);
- floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton);
-
- framan.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
- @Override
- public void onBackStackChanged() {
- Log.d("MainActivity, BusTO", "BACK STACK CHANGED");
- }
- });
+ busStopSearchByIDEditText = findViewById(R.id.busStopSearchByIDEditText);
+ busStopSearchByNameEditText = findViewById(R.id.busStopSearchByNameEditText);
+ progressBar = findViewById(R.id.progressBar);
+ howDoesItWorkTextView = findViewById(R.id.howDoesItWorkTextView);
+ hideHintButton = findViewById(R.id.hideHintButton);
+ swipeRefreshLayout = findViewById(R.id.listRefreshLayout);
+ floatingActionButton = findViewById(R.id.floatingActionButton);
+
+ framan.addOnBackStackChangedListener(() -> Log.d("MainActivity, BusTO", "BACK STACK CHANGED"));
busStopSearchByIDEditText.setSelectAllOnFocus(true);
busStopSearchByIDEditText
- .setOnEditorActionListener(new TextView.OnEditorActionListener() {
- @Override
- public boolean onEditorAction(TextView v, int actionId,
- KeyEvent event) {
- // IME_ACTION_SEARCH alphabetical option
- if (actionId == EditorInfo.IME_ACTION_SEARCH) {
- onSearchClick(v);
- return true;
- }
- return false;
+ .setOnEditorActionListener((v, actionId, event) -> {
+ // IME_ACTION_SEARCH alphabetical option
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ onSearchClick(v);
+ return true;
}
+ return false;
});
busStopSearchByNameEditText
- .setOnEditorActionListener(new TextView.OnEditorActionListener() {
- @Override
- public boolean onEditorAction(TextView v, int actionId,
- KeyEvent event) {
- // IME_ACTION_SEARCH alphabetical option
- if (actionId == EditorInfo.IME_ACTION_SEARCH) {
- onSearchClick(v);
- return true;
- }
- return false;
+ .setOnEditorActionListener((v, actionId, event) -> {
+ // IME_ACTION_SEARCH alphabetical option
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ onSearchClick(v);
+ return true;
}
+ return false;
});
// Called when the layout is pulled down
swipeRefreshLayout
- .setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
- @Override
- public void onRefresh() {
- handler.post(refreshing);
- }
- });
+ .setOnRefreshListener(() -> theHandler.post(refreshing));
/**
* @author Marco Gagino!!!
*/
//swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't
swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500);
fh = new FragmentHelper(this, R.id.listRefreshLayout, R.id.resultFrame);
setSearchModeBusStopID();
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
String busStopDisplayName = null;
Uri data = getIntent().getData();
if (data != null) {
busStopID = getBusStopIDFromUri(data);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
busStopDisplayName = b.getString("bus-stop-display-name");
/**
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
setBusStopSearchByIDEditText(busStopID);
/*
//THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS
// STOP IS ADDED AUTOMATICALLY
Stop nextStop = new Stop(busStopID);
// forcing it as user name even though it could be standard name, it doesn't really matter
nextStop.setStopUserName(busStopDisplayName);
//set stop as last succe
fh.setLastSuccessfullySearchedBusStop(nextStop);
*/
createFragmentForStop(busStopID);
}
//Try (hopefully) database update
- //TODO: Start the service in foreground, check last time it ran before
- DatabaseUpdateService.startDBUpdate(getApplicationContext());
+ //TODO: Check if service shows the notification
+ //Old code for the db update
+ //DatabaseUpdateService.startDBUpdate(getApplicationContext());
+
+
+ PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
+ .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
+ .build())
+ .build();
+ final WorkManager workManager = WorkManager.getInstance(this);
+
+ final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10);
+ if (version >= 0)
+ workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
+ ExistingPeriodicWorkPolicy.KEEP, wr);
+ else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG,
+ ExistingPeriodicWorkPolicy.REPLACE, wr);
/*
Set database update
*/
- updatelistener = new DBStatusManager.OnDBUpdateStatusChangeListener() {
- @Override
- public boolean defaultStatusValue() {
- return true;
- }
+ workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
+ .observe(this, workInfoList -> {
+ // If there are no matching work info, do nothing
+ if (workInfoList == null || workInfoList.isEmpty()) {
+ return;
+ }
+ Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList);
- @Override
- public void onDBStatusChanged(boolean updating) {
+ boolean showProgress = false;
+ for (WorkInfo workInfo : workInfoList) {
+ if (workInfo.getState() == WorkInfo.State.RUNNING) {
+ showProgress = true;
+ }
+ }
- if (updating) {
- createDefaultSnackbar();
- } else if (snackbar != null) {
- snackbar.dismiss();
- snackbar = null;
- }
+ if (showProgress) {
+ createDefaultSnackbar();
+ } else {
+ if(snackbar!=null) {
+ snackbar.dismiss();
+ snackbar = null;
+ }
+ }
+
+ });
- }
- };
- prefsManager = new DBStatusManager(getApplicationContext(), updatelistener);
- prefsManager.registerListener();
//locationHandler = new GPSLocationAdapter(getApplicationContext());
//--------- NEARBY STOPS--------//
//SETUP LOCATION
locmgr = (LocationManager) getSystemService(LOCATION_SERVICE);
cr.setAccuracy(Criteria.ACCURACY_FINE);
cr.setAltitudeRequired(false);
cr.setBearingRequired(false);
cr.setCostAllowed(true);
cr.setPowerRequirement(Criteria.NO_REQUIREMENT);
//We want the nearby bus stops!
- handler.post(new NearbyStopsRequester());
+ theHandler.post(new NearbyStopsRequester());
//If there are no providers available, then, wait for them
Log.d("MainActivity", "Created");
}
/**
* Reload bus stop timetable when it's fulled resumed from background.
*/
/**
* @Override protected void onPostResume() {
* super.onPostResume();
* Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop());
* if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) {
* setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID);
* new 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);
- if (updatelistener != null && prefsManager != null) prefsManager.unregisterListener();
locmgr.removeUpdates(locListener);
}
@Override
protected void onResume() {
super.onResume();
fh.setBlockAllActivities(false);
- if (updatelistener != null && prefsManager != null) {
- prefsManager.registerListener();
- if (prefsManager.isDBUpdating(true)) {
- createDefaultSnackbar();
- }
- }
+ //TODO: check if current LiveData-bound observer works
if (pendingNearbyStopsRequest)
- handler.post(new NearbyStopsRequester());
+ theHandler.post(new NearbyStopsRequester());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
actionHelpMenuItem = menu.findItem(R.id.action_help);
return true;
}
/**
* Callback fired when a MenuItem is selected
*
* @param item
* @return
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
switch (item.getItemId()) {
case android.R.id.home:
// Respond to the action bar's Up/Home button
NavUtils.navigateUpFromSameTask(this);
return true;
case R.id.action_help:
showHints();
return true;
case R.id.action_favorites:
startActivity(new Intent(ActivityMain.this, ActivityFavorites.class));
return true;
case R.id.action_map:
- startActivity(new Intent(ActivityMain.this, ActivityMap.class));
+ //ensure storage permission is granted
+ final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
+ int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
+ switch (result) {
+ case PERMISSION_OK:
+ startActivity(new Intent(ActivityMain.this, ActivityMap.class));
+ break;
+ case PERMISSION_ASKING:
+ permissionDoneRunnables.put(permission,
+ () -> startActivity(new Intent(ActivityMain.this, ActivityMap.class)));
+ break;
+ case PERMISSION_NEG_CANNOT_ASK:
+ Resources res = getResources();
+ String storage_perm = res.getString(R.string.storage_permission);
+ String text = res.getString(R.string.too_many_permission_asks, storage_perm);
+ Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show();
+ }
return true;
+
case R.id.action_about:
startActivity(new Intent(ActivityMain.this, ActivityAbout.class));
return true;
case R.id.action_news:
openIceweasel("https://gitpull.it/w/librebusto/#how-to-get-news");
return true;
case R.id.action_bugs:
openIceweasel("https://gitpull.it/w/librebusto/#how-to-create-a-bug-feature");
return true;
case R.id.action_source:
openIceweasel("https://gitpull.it/w/librebusto/#how-to-hack-busto");
return true;
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html");
return true;
case R.id.action_settings:
Log.d("MAINBusTO", "Pressed button preferences");
startActivity(new Intent(ActivityMain.this, ActivitySettings.class));
}
return super.onOptionsItemSelected(item);
}
/**
* OK this is pure shit
*
* @param v View clicked
*/
public void onSearchClick(View v) {
if (searchMode == SEARCH_BY_ID) {
String busStopID = busStopSearchByIDEditText.getText().toString();
-
createFragmentForStop(busStopID);
} else { // searchMode == SEARCH_BY_NAME
String query = busStopSearchByNameEditText.getText().toString();
//new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper);
new AsyncDataDownload(fh, stopsFinderByNames).execute(query);
}
}
/**
* PERMISSION STUFF
**/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSION_REQUEST_POSITION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
setOption(LOCATION_PERMISSION_GIVEN, true);
//if we sent a request for a new NearbyStopsFragment
if (pendingNearbyStopsRequest) {
pendingNearbyStopsRequest = false;
- handler.post(new NearbyStopsRequester());
+ theHandler.post(new NearbyStopsRequester());
}
} 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 createFragmentForStop(String ID) {
if (ID == null || ID.length() <= 0) {
// we're still in UI thread, no need to mess with Progress
- showMessage(R.string.insert_bus_stop_number_error);
+ showToastMessage(R.string.insert_bus_stop_number_error, true);
toggleSpinner(false);
} else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame);
if (fragment.getStopID() != null && fragment.getStopID().equals(ID)){
// Run with previous fetchers
//fragment.getCurrentFetchers().toArray()
new AsyncDataDownload(fh,fragment.getCurrentFetchersAsArray()).execute(ID);
} else{
new AsyncDataDownload(fh, arrivalsFetchers).execute(ID);
}
}
else {
new AsyncDataDownload(fh,arrivalsFetchers).execute(ID);
Log.d("MainActiv", "Started search for arrivals of stop " + ID);
}
}
/**
* 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);
createFragmentForStop(busStopID);
}
public void onHideHint(View v) {
hideHints();
setOption(OPTION_SHOW_LEGEND, false);
}
public void onToggleKeyboardLayout(View v) {
if (searchMode == SEARCH_BY_NAME) {
setSearchModeBusStopID();
if (busStopSearchByIDEditText.requestFocus()) {
showKeyboard();
}
} else { // searchMode == SEARCH_BY_ID
setSearchModeBusStopName();
if (busStopSearchByNameEditText.requestFocus()) {
showKeyboard();
}
}
}
private void createDefaultSnackbar() {
if (snackbar == null) {
snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE);
}
snackbar.show();
}
///////////////////////////////// POSITION STUFF//////////////////////////////////////////////
private void resolveStopRequest(String provider) {
Log.d(DEBUG_TAG, "Provider " + provider + " got enabled");
if (locmgr != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) {
pendingNearbyStopsRequest = false;
- handler.post(new NearbyStopsRequester());
+ theHandler.post(new NearbyStopsRequester());
}
}
final LocationListener locListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
Log.d(DEBUG_TAG, "Location changed");
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d(DEBUG_TAG, "Location provider status: " + status);
if (status == LocationProvider.AVAILABLE) {
resolveStopRequest(provider);
}
}
@Override
public void onProviderEnabled(String provider) {
resolveStopRequest(provider);
}
@Override
public void onProviderDisabled(String provider) {
}
};
+ /**
+ * Run location requests separately and asynchronously
+ */
class NearbyStopsRequester implements Runnable {
- @SuppressLint("MissingPermission")
@Override
public void run() {
final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false);
-
- if (!canRunPosition) {
- pendingNearbyStopsRequest = true;
- assertLocationPermissions();
- return;
+ final boolean noPermission = ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+ ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED;
+
+ //if we don't have the permission, we have to ask for it, if we haven't
+ // asked too many times before
+ if (noPermission) {
+ if (!canRunPosition) {
+ pendingNearbyStopsRequest = true;
+ assertLocationPermissions();
+ Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission);
+ return;
+ } else {
+ Toast.makeText(getApplicationContext(), "Asked for permission position too many times", Toast.LENGTH_LONG).show();
+ }
} else setOption(LOCATION_PERMISSION_GIVEN, true);
LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE);
if (locManager == null) {
Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment");
return;
}
if (anyLocationProviderMatchesCriteria(locManager, cr, true) && fh.getLastSuccessfullySearchedBusStop() == null) {
//Go ahead with the request
Log.d("mainActivity", "Recreating stop fragment");
swipeRefreshLayout.setVisibility(View.VISIBLE);
NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS);
Fragment oldFrag = framan.findFragmentById(R.id.resultFrame);
FragmentTransaction ft = framan.beginTransaction();
if (oldFrag != null)
ft.remove(oldFrag);
ft.add(R.id.resultFrame, fragment, "nearbyStop_correct");
ft.commit();
framan.executePendingTransactions();
pendingNearbyStopsRequest = false;
} else if (!anyLocationProviderMatchesCriteria(locManager, cr, true)) {
//Wait for the providers
Log.d(DEBUG_TAG, "Queuing position request");
pendingNearbyStopsRequest = true;
+
locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10, 0.1f, locListener);
}
}
}
private boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled) {
List providers = mng.getProviders(cr, enabled);
Log.d(DEBUG_TAG, "Getting enabled location providers: ");
for (String s : providers) {
Log.d(DEBUG_TAG, "Provider " + s);
}
return providers.size() > 0;
}
///////////////////////////////// OTHER STUFF //////////////////////////////////////////////////
/**
* Get the last successfully searched bus stop or NULL
*
* @return
*/
@Override
public Stop getLastSuccessfullySearchedBusStop() {
return fh.getLastSuccessfullySearchedBusStop();
}
/**
* Get the last successfully searched bus stop ID or NULL
*
* @return
*/
@Override
public String getLastSuccessfullySearchedBusStopID() {
Stop stop = getLastSuccessfullySearchedBusStop();
return stop == null ? null : stop.ID;
}
/**
* Update the star "Add to favorite" icon
*/
@Override
public void updateStarIconFromLastBusStop() {
// no favorites no party!
addToFavorites = (ImageButton) findViewById(R.id.addToFavorites);
if (addToFavorites == null) {
Log.d("MainActivity", "Why the fuck the star is not here?!");
return;
}
// check if there is a last Stop
String stopID = getLastSuccessfullySearchedBusStopID();
if (stopID == null) {
addToFavorites.setVisibility(View.INVISIBLE);
} else {
// filled or outline?
if (isStopInFavorites(stopID)) {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
} else {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
addToFavorites.setVisibility(View.VISIBLE);
}
}
/**
* Check if the last Bus Stop is in the favorites
*
* @return
*/
public boolean isStopInFavorites(String busStopId) {
boolean found = false;
// no stop no party
if (busStopId != null) {
SQLiteDatabase userDB = new UserDB(getApplicationContext()).getReadableDatabase();
found = UserDB.isStopInFavorites(userDB, busStopId);
}
return found;
}
/**
* Add the last Stop to favorites
*/
@Override
public void toggleLastStopToFavorites() {
Stop stop = getLastSuccessfullySearchedBusStop();
if (stop != null) {
// toggle the status in background
new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE) {
/**
* Callback fired when the Stop is saved in the favorites
* @param result
*/
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
// update the star icon
updateStarIconFromLastBusStop();
}
}.execute(stop);
} else {
// this case have no sense, but just immediately update the favorite icon
updateStarIconFromLastBusStop();
}
}
@Override
public void showFloatingActionButton(boolean yes) {
if (yes) floatingActionButton.show();
else floatingActionButton.hide();
}
@Override
public void enableRefreshLayout(boolean yes) {
swipeRefreshLayout.setEnabled(yes);
}
////////////////////////////////////// GUI HELPERS /////////////////////////////////////////////
- @Override
public void showKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText;
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
- @Override
- public void showMessage(int messageID) {
- Toast.makeText(getApplicationContext(), messageID, Toast.LENGTH_SHORT).show();
- }
-
private void setSearchModeBusStopID() {
searchMode = SEARCH_BY_ID;
busStopSearchByNameEditText.setVisibility(View.GONE);
busStopSearchByNameEditText.setText("");
busStopSearchByIDEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.alphabetical);
}
private void setSearchModeBusStopName() {
searchMode = SEARCH_BY_NAME;
busStopSearchByIDEditText.setVisibility(View.GONE);
busStopSearchByIDEditText.setText("");
busStopSearchByNameEditText.setVisibility(View.VISIBLE);
floatingActionButton.setImageResource(R.drawable.numeric);
}
/**
* Having that cursor at the left of the edit text makes me cancer.
*
* @param busStopID bus stop ID
*/
private void setBusStopSearchByIDEditText(String busStopID) {
busStopSearchByIDEditText.setText(busStopID);
busStopSearchByIDEditText.setSelection(busStopID.length());
}
private void showHints() {
howDoesItWorkTextView.setVisibility(View.VISIBLE);
hideHintButton.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(false);
}
private void hideHints() {
howDoesItWorkTextView.setVisibility(View.GONE);
hideHintButton.setVisibility(View.GONE);
actionHelpMenuItem.setVisible(true);
}
//TODO: toggle spinner from mainActivity
@Override
public void toggleSpinner(boolean enable) {
if (enable) {
//already set by the RefreshListener when needed
//swipeRefreshLayout.setRefreshing(true);
progressBar.setVisibility(View.VISIBLE);
} else {
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
}
}
private void prepareGUIForBusLines() {
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(true);
}
private void prepareGUIForBusStops() {
swipeRefreshLayout.setEnabled(false);
swipeRefreshLayout.setVisibility(View.VISIBLE);
actionHelpMenuItem.setVisible(false);
}
/**
* This provides a temporary fix to make the transition
* to a single asynctask go smoother
*
* @param fragmentType the type of fragment created
*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
hideKeyboard();
//if we are getting results, already, stop waiting for nearbyStops
if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) {
locmgr.removeUpdates(locListener);
pendingNearbyStopsRequest = false;
}
if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType");
else
switch (fragmentType) {
case ARRIVALS:
prepareGUIForBusLines();
if (getOption(OPTION_SHOW_LEGEND, true)) {
showHints();
}
break;
case STOPS:
prepareGUIForBusStops();
break;
default:
Log.e("BusTO Activity", "Called readyGUI with unsupported type of Fragment");
return;
}
// Shows hints
}
/**
* Open an URL in the default browser.
*
* @param url URL
*/
public void openIceweasel(String url) {
Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(browserIntent1);
}
///////////////////// INTENT HELPER ////////////////////////////////////////////////////////////
/**
* Try to extract the bus stop ID from a URi
*
* @param uri The URL
* @return bus stop ID or null
*/
public static String getBusStopIDFromUri(Uri uri) {
String busStopID;
// everithing catches fire when passing null to a switch.
String host = uri.getHost();
if (host == null) {
Log.e("ActivityMain", "Not an URL: " + uri);
return null;
}
switch (host) {
case "m.gtt.to.it":
// http://m.gtt.to.it/m/it/arrivi.jsp?n=1254
busStopID = uri.getQueryParameter("n");
if (busStopID == null) {
Log.e("ActivityMain", "Expected ?n from: " + uri);
}
break;
case "www.gtt.to.it":
case "gtt.to.it":
// http://www.gtt.to.it/cms/percorari/arrivi?palina=1254
busStopID = uri.getQueryParameter("palina");
if (busStopID == null) {
Log.e("ActivityMain", "Expected ?palina from: " + uri);
}
break;
default:
Log.e("ActivityMain", "Unexpected intent URL: " + uri);
busStopID = null;
}
return busStopID;
}
public void changeStarType(String stopID) {
if (isStopInFavorites(stopID)) {
changeStarFilled();
} else {
changeStarOutline();
}
}
public void changeStarFilled() {
addToFavorites.setImageResource(R.drawable.ic_star_filled);
}
public void changeStarOutline() {
addToFavorites.setImageResource(R.drawable.ic_star_outline);
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivityMap.java b/src/it/reyboz/bustorino/ActivityMap.java
index 0b87001..4e6713f 100644
--- a/src/it/reyboz/bustorino/ActivityMap.java
+++ b/src/it/reyboz/bustorino/ActivityMap.java
@@ -1,393 +1,429 @@
/*
BusTO Activities
Copyright (C) 2020 Andrea Ugo e 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;
+import android.Manifest;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
-import android.support.annotation.RequiresApi;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.preference.PreferenceManager;
-import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.preference.PreferenceManager;
+
+import it.reyboz.bustorino.middleware.GeneralActivity;
import it.reyboz.bustorino.middleware.NextGenDB;
+
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
import java.util.*;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.map.CustomInfoWindow;
-import it.reyboz.bustorino.middleware.StopsDB;
-public class ActivityMap extends AppCompatActivity {
+public class ActivityMap extends GeneralActivity {
private static final String TAG = "Busto-MapActivity";
private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom";
private static final String MAP_CENTER_LAT_KEY = "map-center-lat";
private static final String MAP_CENTER_LON_KEY = "map-center-lon";
public static final String BUNDLE_LATIT = "lat";
public static final String BUNDLE_LONGIT = "lon";
public static final String BUNDLE_NAME = "name";
public static final String BUNDLE_ID = "ID";
private static final double DEFAULT_CENTER_LAT = 45.0708;
private static final double DEFAULT_CENTER_LON = 7.6858;
private static final double POSITION_FOUND_ZOOM = 18.3;
private HashSet shownStops = null;
private MapView map = null;
public Context ctx;
private MyLocationNewOverlay mLocationOverlay = null;
private FolderOverlay stopsFolderOverlay = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
- @Override public void onCreate(Bundle savedInstanceState) {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//handle permissions first, before map is created. not depicted here
- //load/initialize the osmdroid configuration, this can be done
+ //load/initialize the osmdroid configuration
+
+
ctx = getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
//setting this before the layout is inflated is a good idea
//it 'should' ensure that the map has a writable location for the map cache, even without permissions
//if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath
//see also StorageUtils
//note, the load method also sets the HTTP User Agent to your application's package name, abusing osm's tile servers will get you banned based on this string
//inflate and create the map
setContentView(R.layout.activity_map);
map = (MapView) findViewById(R.id.map);
map.setTileSource(TileSourceFactory.MAPNIK);
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true);
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true);
btCenterMap = (ImageButton) findViewById(R.id.ic_center_map);
btFollowMe = (ImageButton) findViewById(R.id.ic_follow_me);
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
// take the parameters if it's called from other Activities
Bundle b = getIntent().getExtras();
startMap(b, savedInstanceState);
// on drag and zoom reload the markers
map.addMapListener(new DelayedMapListener(new MapListener() {
@Override
public boolean onScroll(ScrollEvent paramScrollEvent) {
loadMarkers();
return true;
}
@Override
public boolean onZoom(ZoomEvent event) {
loadMarkers();
return true;
}
}));
-
btCenterMap.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "centerMap clicked ");
final GeoPoint myPosition = mLocationOverlay.getMyLocation();
map.getController().animateTo(myPosition);
}
});
btFollowMe.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "btFollowMe clicked ");
if (!mLocationOverlay.isFollowLocationEnabled()) {
mLocationOverlay.enableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
} else {
mLocationOverlay.disableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me);
}
}
});
}
+
public void startMap(Bundle incoming, Bundle savedInstanceState) {
//parse incoming bundle
GeoPoint marker = null;
String name = null;
String ID = null;
- if(incoming != null) {
+ if (incoming != null) {
double lat = incoming.getDouble(BUNDLE_LATIT);
double lon = incoming.getDouble(BUNDLE_LONGIT);
marker = new GeoPoint(lat, lon);
name = incoming.getString(BUNDLE_NAME);
ID = incoming.getString(BUNDLE_ID);
}
shownStops = new HashSet<>();
// move the map on the marker position or on a default view point: Turin, Piazza Castello
// and set the start zoom
IMapController mapController = map.getController();
GeoPoint startPoint = null;
+ boolean havePositionPermission = true;
+
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ askForPermissionIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION, PERMISSION_REQUEST_POSITION);
+ havePositionPermission = false;
+ }
+
if (marker != null) {
startPoint = marker;
mapController.setZoom(POSITION_FOUND_ZOOM);
- }
- else if (savedInstanceState != null) {
+ } else if (savedInstanceState != null || !havePositionPermission) {
mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY));
mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY),
savedInstanceState.getDouble(MAP_CENTER_LON_KEY)));
} else {
boolean found = false;
LocationManager locationManager =
(LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
- if(locationManager!=null) {
+ if (locationManager != null) {
+
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
mapController.setZoom(POSITION_FOUND_ZOOM);
startPoint = new GeoPoint(userLocation);
found = true;
}
}
if(!found){
startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON);
mapController.setZoom(16.0);
}
}
// set the minimum zoom level
map.setMinZoomLevel(15.0);
//add contingency check (shouldn't happen..., but)
if (startPoint != null) {
mapController.setCenter(startPoint);
}
// Location Overlay
// from OpenBikeSharing (THANK GOD)
GpsMyLocationProvider imlp = new GpsMyLocationProvider(this.getBaseContext());
imlp.setLocationUpdateMinDistance(5);
imlp.setLocationUpdateMinTime(2000);
this.mLocationOverlay = new MyLocationNewOverlay(imlp,map);
mLocationOverlay.enableMyLocation();
mLocationOverlay.enableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
mLocationOverlay.setOptionsMenuEnabled(true);
/*
mLocationOverlay.runOnFirstFix(() -> {
mapController.setCenter(mLocationOverlay.getMyLocation());
mapController.animateTo(mLocationOverlay.getMyLocation());
});
*/
map.getOverlays().add(this.mLocationOverlay);
//add stops overlay
map.getOverlays().add(this.stopsFolderOverlay);
loadMarkers();
if (marker != null) {
// make a marker with the info window open for the searched marker
makeMarker(startPoint, name , ID, true);
}
}
public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) {
// add a marker
Marker marker = new Marker(map);
// set custom info window as info window
CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName);
marker.setInfoWindow(popup);
// make the marker clickable
marker.setOnMarkerClickListener((thisMarker, mapView) -> {
if (thisMarker.isInfoWindowOpen()) {
// on second click
// create an intent with these extras
Intent intent = new Intent(ActivityMap.this, ActivityMain.class);
Bundle b = new Bundle();
b.putString("bus-stop-ID", ID);
b.putString("bus-stop-display-name", stopName);
intent.putExtras(b);
intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
// start ActivityMain with the previous intent
startActivity(intent);
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map);
// show this particular info window
thisMarker.showInfoWindow();
// move the map to its position
map.getController().animateTo(thisMarker.getPosition());
}
return true;
});
// set its position
marker.setPosition(geoPoint);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
// add to it an icon
marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
marker.setSnippet(ID);
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow();
}
return marker;
}
public void loadMarkers() {
// get rid of the previous markers
//map.getOverlays().clear();
//stopsFolderOverlay = new FolderOverlay();
List stopsOverlays = stopsFolderOverlay.getItems();
/*if (stopsOverlays != null){
stopsOverlays.clear();
}*/
// get the top, bottom, left and right screen's coordinate
BoundingBox bb = map.getBoundingBox();
double latFrom = bb.getLatSouth();
double latTo = bb.getLatNorth();
double lngFrom = bb.getLonWest();
double lngTo = bb.getLonEast();
// get the stops located in those coordinates
/*
StopsDB stopsDB = new StopsDB(ctx);
stopsDB.openIfNeeded();
Stop[] stops = stopsDB.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo);
stopsDB.closeIfNeeded();
*/
NextGenDB dbHelper = new NextGenDB(ctx);
Stop[] stops = dbHelper.queryAllInsideMapView(latFrom, latTo, lngFrom, lngTo);
// add new markers of those stops
for (Stop stop : stops) {
if (shownStops.contains(stop.ID)){
continue;
}
try{
stop.getLatitude();
stop.getLongitude();
} catch (NullPointerException e) {
Log.e(TAG,"Stop "+stop.ID+ " gives null coordinates");
e.printStackTrace();
continue;
}
shownStops.add(stop.ID);
GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude());
Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false);
stopsFolderOverlay.add(stopMarker);
}
}
protected boolean detachMapFromPosition(){
if (mLocationOverlay.isFollowLocationEnabled()) {
mLocationOverlay.disableFollowLocation();
btFollowMe.setImageResource(R.drawable.ic_follow_me);
return true;
} return false;
}
public void onResume(){
super.onResume();
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
//Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this));
map.onResume(); //needed for compass, my location overlays, v6.0.0 and up
mLocationOverlay.enableMyLocation();
}
public void onPause(){
super.onPause();
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
//Configuration.getInstance().save(this, prefs);
map.onPause(); //needed for compass, my location overlays, v6.0.0 and up
mLocationOverlay.disableMyLocation();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble());
outState.putDouble(MAP_CENTER_LAT_KEY, map.getMapCenter().getLatitude());
outState.putDouble(MAP_CENTER_LON_KEY, map.getMapCenter().getLongitude());
}
+ /**
+ * PERMISSION STUFF
+ **/
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case PERMISSION_REQUEST_POSITION:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ setOption(LOCATION_PERMISSION_GIVEN, true);
+ //if we sent a request for a new NearbyStopsFragment
+
+
+ } else {
+ //permission denied
+ setOption(LOCATION_PERMISSION_GIVEN, false);
+ }
+ break;
+ //add other cases for permissions
+ }
+
+ }
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivitySettings.java b/src/it/reyboz/bustorino/ActivitySettings.java
index 17cdf61..1731196 100644
--- a/src/it/reyboz/bustorino/ActivitySettings.java
+++ b/src/it/reyboz/bustorino/ActivitySettings.java
@@ -1,37 +1,37 @@
package it.reyboz.bustorino;
import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import it.reyboz.bustorino.fragments.SettingsFragment;
public class ActivitySettings extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ActionBar ab = getSupportActionBar();
if(ab!=null) {
ab.setIcon(R.drawable.ic_launcher);
ab.setDisplayHomeAsUpEnabled(true);
} else {
Log.e("SETTINGS_ACTIV","ACTION BAR IS NULL");
}
FragmentManager framan = getSupportFragmentManager();
FragmentTransaction ft = framan.beginTransaction();
ft.replace(R.id.setting_container,new SettingsFragment());
ft.commit();
}
}
/**
* Interesting thing
* Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
* .setAction("Action", null).show();
*/
diff --git a/src/it/reyboz/bustorino/BustoApp.java b/src/it/reyboz/bustorino/BustoApp.java
new file mode 100644
index 0000000..4adc9cd
--- /dev/null
+++ b/src/it/reyboz/bustorino/BustoApp.java
@@ -0,0 +1,36 @@
+package it.reyboz.bustorino;
+
+import android.app.Application;
+import android.content.Context;
+
+import org.acra.ACRA;
+import org.acra.BuildConfig;
+import org.acra.annotation.AcraCore;
+import org.acra.annotation.AcraDialog;
+import org.acra.annotation.AcraMailSender;
+import org.acra.config.CoreConfigurationBuilder;
+import org.acra.config.DialogConfigurationBuilder;
+import org.acra.config.MailSenderConfigurationBuilder;
+import org.acra.data.StringFormat;
+
+
+public class BustoApp extends Application {
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+
+ CoreConfigurationBuilder builder = new CoreConfigurationBuilder(this);
+ builder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.JSON)
+ .setDeleteUnapprovedReportsOnApplicationStart(true);
+ builder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setMailTo("gtt@succhia.cz")
+ .setReportFileName(it.reyboz.bustorino.BuildConfig.VERSION_NAME +"_report.json")
+ .setResBody(R.string.acra_email_message)
+ .setEnabled(true);
+ builder.getPluginConfigurationBuilder(DialogConfigurationBuilder.class).setResText(R.string.message_crash)
+ .setResTheme(R.style.AppTheme)
+ .setEnabled(true);
+
+ ACRA.init(this, builder);
+ }
+}
diff --git a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java
index ebc6ab5..9d65e71 100644
--- a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java
+++ b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java
@@ -1,185 +1,185 @@
/*
BusTO - UI components
Copyright (C) 2017 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.adapters;
import android.content.Context;
import android.location.Location;
-import android.support.annotation.Nullable;
-import android.support.v4.util.Pair;
-import android.support.v7.widget.RecyclerView;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.fragments.FragmentListener;
import it.reyboz.bustorino.util.RoutePositionSorter;
import it.reyboz.bustorino.util.StopSorterByDistance;
import java.util.*;
public class ArrivalsStopAdapter extends RecyclerView.Adapter {
private final static int layoutRes = R.layout.arrivals_nearby_card;
//private List stops;
private @Nullable Location userPosition;
private FragmentListener listener;
private List< Pair > routesPairList = new ArrayList<>();
private Context context;
//Maximum number of stops to keep
private final int MAX_STOPS = 20; //TODO: make it programmable
public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListener fragmentListener, Context con, @Nullable Location pos) {
listener = fragmentListener;
userPosition = pos;
this.routesPairList = routesPairList;
context = con.getApplicationContext();
resetListAndPosition();
// if(paline!=null)
//resetRoutesPairList(paline);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
//DO THE ACTUAL WORK TO PUT THE DATA
if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS
final Pair stopRoutePair = routesPairList.get(position);
if(stopRoutePair!=null){
final Stop stop = stopRoutePair.first;
final Route r = stopRoutePair.second;
final Double distance = stop.getDistanceFromLocation(userPosition);
if(distance!=Double.POSITIVE_INFINITY){
holder.distancetextView.setText(distance.intValue()+" m");
} else {
holder.distancetextView.setVisibility(View.GONE);
}
final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID);
holder.stopNameView.setText(stopText);
//final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione);
holder.lineNameTextView.setText(r.getNameForDisplay());
/* EXPERIMENTS
if(r.destinazione==null || r.destinazione.trim().isEmpty()){
holder.lineDirectionTextView.setVisibility(View.GONE);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams();
params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId());
holder.arrivalsDescriptionTextView.setLayoutParams(params);
} else {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams();
params.removeRule(RelativeLayout.RIGHT_OF);
holder.arrivalsDescriptionTextView.setLayoutParams(params);
holder.lineDirectionTextView.setVisibility(View.VISIBLE);
}
*/
holder.lineDirectionTextView.setText(r.destinazione);
holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true));
holder.stopID =stop.ID;
} else {
Log.w("SquareStopAdapter","!! The selected stop is null !!");
}
}
@Override
public int getItemCount() {
return routesPairList.size();
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView lineNameTextView;
TextView lineDirectionTextView;
TextView stopNameView;
TextView arrivalsDescriptionTextView;
TextView arrivalsTextView;
TextView distancetextView;
String stopID;
ViewHolder(View holdView){
super(holdView);
holdView.setOnClickListener(this);
lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView);
lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView);
stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName);
arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView);
arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView);
distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView);
}
@Override
public void onClick(View v) {
listener.createFragmentForStop(stopID);
}
}
public void resetRoutesPairList(List stopList){
Collections.sort(stopList,new StopSorterByDistance(userPosition));
this.routesPairList = new ArrayList<>(stopList.size());
int maxNum = Math.min(MAX_STOPS, stopList.size());
for(Palina p: stopList.subList(0,maxNum)){
//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));
}
}
}
public void setUserPosition(@Nullable Location userPosition) {
this.userPosition = userPosition;
}
public void setRoutesPairListAndPosition(List> routesPairList, @Nullable Location pos) {
if(routesPairList!=null)
this.routesPairList = routesPairList;
if(pos!=null){
this.userPosition = pos;
}
resetListAndPosition();
}
private void resetListAndPosition(){
Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition));
//All of this to get only the first occurrences of a line (name & direction)
ListIterator> iterator = routesPairList.listIterator();
Set> allRoutesDirections = new HashSet<>();
while(iterator.hasNext()){
final Pair stopRoutePair = iterator.next();
final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(),stopRoutePair.second.destinazione);
if(allRoutesDirections.contains(routeNameDirection)){
iterator.remove();
} else {
allRoutesDirections.add(routeNameDirection);
}
}
}
}
diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
index 260a1fb..88d8ac9 100644
--- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
+++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java
@@ -1,146 +1,146 @@
/*
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.adapters;
import android.content.Context;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
/**
* This once was a ListView Adapter for BusLine[].
*
* Thanks to Framentos developers for the guide:
* http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/#
*
* @author Valerio Bozzolan
* @author Ludovico Pavesi
*/
public class PalinaAdapter extends ArrayAdapter {
private LayoutInflater li;
private static int row_layout = R.layout.entry_bus_line_passage;
private static final int metroBg = R.drawable.route_background_metro;
private static final int busBg = R.drawable.route_background_bus;
private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance;
private static final int busIcon = R.drawable.bus;
private static final int trainIcon = R.drawable.subway;
private static final int tramIcon = R.drawable.tram;
//private static final int cityIcon = R.drawable.city;
// hey look, a pattern!
private static class ViewHolder {
TextView rowStopIcon;
TextView rowRouteDestination;
TextView rowRouteTimetable;
}
public PalinaAdapter(Context context, Palina p) {
super(context, row_layout, p.queryAllRoutes());
li = LayoutInflater.from(context);
}
/**
* Some parts taken from the AdapterBusLines class.
* Some parts inspired by these enlightening tutorials:
* http://www.simplesoft.it/android/guida-agli-adapter-e-le-listview-in-android.html
* https://www.codeofaninja.com/2013/09/android-viewholder-pattern-example.html
* And some other bits and bobs TIRATI FUORI DAL NULLA CON L'INTUIZIONE INTELLETTUALE PERCHÉ
* SEMBRA CHE NESSUNO ABBIA LA MINIMA IDEA DI COME FUNZIONA UN ADAPTER SU ANDROID.
*/
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
ViewHolder vh;
if(convertView == null) {
// INFLATE!
// setting a parent here is not supported and causes a fatal exception, apparently.
convertView = li.inflate(row_layout, null);
// STORE TEXTVIEWS!
vh = new ViewHolder();
vh.rowStopIcon = (TextView) convertView.findViewById(R.id.routeID);
vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination);
vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere);
// STORE VIEWHOLDER IN\ON\OVER\UNDER\ABOVE\BESIDE THE VIEW!
convertView.setTag(vh);
} else {
// RECOVER THIS STUFF!
vh = (ViewHolder) convertView.getTag();
}
Route route = getItem(position);
vh.rowStopIcon.setText(route.getNameForDisplay());
if(route.destinazione==null || route.destinazione.length() == 0) {
vh.rowRouteDestination.setVisibility(View.GONE);
} else {
// View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible.
vh.rowRouteDestination.setVisibility(View.VISIBLE);
vh.rowRouteDestination.setText(route.destinazione);
}
switch (route.type) {
//UNKNOWN = BUS for the moment
case UNKNOWN:
case BUS:
default:
// convertView could contain another background, reset it
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
break;
case LONG_DISTANCE_BUS:
vh.rowStopIcon.setBackgroundResource(extraurbanoBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
break;
case METRO:
vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
vh.rowStopIcon.setBackgroundResource(metroBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
break;
case RAILWAY:
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
break;
case TRAM: // never used but whatever.
vh.rowStopIcon.setBackgroundResource(busBg);
vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0);
break;
}
List passaggi = route.passaggi;
if(passaggi.size() == 0) {
vh.rowRouteTimetable.setText(R.string.no_passages);
} else {
vh.rowRouteTimetable.setText(route.getPassaggiToString());
}
return convertView;
}
}
diff --git a/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java
index 258b1ee..e4ab98b 100644
--- a/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java
+++ b/src/it/reyboz/bustorino/adapters/SquareStopAdapter.java
@@ -1,129 +1,128 @@
/*
BusTO - UI components
Copyright (C) 2017 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.adapters;
-import android.content.Context;
import android.location.Location;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.util.StopSorterByDistance;
import it.reyboz.bustorino.fragments.FragmentListener;
import java.util.Collections;
import java.util.List;
public class SquareStopAdapter extends RecyclerView.Adapter {
private final static int layoutRes = R.layout.stop_card;
//private List stops;
private @Nullable Location userPosition;
private FragmentListener listener;
private List stops;
public SquareStopAdapter(@Nullable List stopList, FragmentListener fragmentListener, @Nullable Location pos) {
listener = fragmentListener;
userPosition = pos;
stops = stopList;
}
@Override
public SquareViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false);
//sort the stops by distance
if(stops != null && stops.size() > 0)
Collections.sort(stops,new StopSorterByDistance(userPosition));
return new SquareViewHolder(view);
}
@Override
public void onBindViewHolder(SquareViewHolder holder, int position) {
//DO THE ACTUAL WORK TO PUT THE DATA
if(stops==null || stops.size() == 0) return; //NO STOPS
final Stop stop = stops.get(position);
if(stop!=null){
if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){
Double distance = stop.getDistanceFromLocation(userPosition);
holder.distancetextView.setText(distance.intValue()+" m");
} else {
holder.distancetextView.setVisibility(View.GONE);
}
holder.stopNameView.setText(stop.getStopDisplayName());
holder.stopIDView.setText(stop.ID);
String whatStopsHere = stop.routesThatStopHereToString();
if(whatStopsHere == null) {
holder.routesView.setVisibility(View.GONE);
} else {
holder.routesView.setText(whatStopsHere);
holder.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern
}
holder.stopID =stop.ID;
} else {
Log.w("SquareStopAdapter","!! The selected stop is null !!");
}
}
@Override
public int getItemCount() {
return stops.size();
}
class SquareViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView stopIDView;
TextView stopNameView;
TextView routesView;
TextView distancetextView;
String stopID;
SquareViewHolder(View holdView){
super(holdView);
holdView.setOnClickListener(this);
stopIDView = (TextView) holdView.findViewById(R.id.stop_numberText);
stopNameView = (TextView) holdView.findViewById(R.id.stop_nameText);
routesView = (TextView) holdView.findViewById(R.id.stop_linesText);
distancetextView = (TextView) holdView.findViewById(R.id.stop_distanceTextView);
}
@Override
public void onClick(View v) {
listener.createFragmentForStop(stopID);
}
}
public void setStops(List stops) {
this.stops = stops;
}
public void setUserPosition(@Nullable Location userPosition) {
this.userPosition = userPosition;
}
/*
@Override
public Stop getItem(int position) {
return stops.get(position);
}
*/
}
diff --git a/src/it/reyboz/bustorino/adapters/StopAdapter.java b/src/it/reyboz/bustorino/adapters/StopAdapter.java
index 1601e00..15a6505 100644
--- a/src/it/reyboz/bustorino/adapters/StopAdapter.java
+++ b/src/it/reyboz/bustorino/adapters/StopAdapter.java
@@ -1,118 +1,118 @@
/*
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.adapters;
import android.content.Context;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Stop;
/**
* @see PalinaAdapter
*/
public class StopAdapter extends ArrayAdapter {
private LayoutInflater li;
private static int row_layout = R.layout.entry_bus_stop;
private static final int busIcon = R.drawable.bus;
private static final int trainIcon = R.drawable.subway;
private static final int tramIcon = R.drawable.tram;
private static final int cityIcon = R.drawable.city;
private static class ViewHolder {
TextView busStopIDTextView;
TextView busStopNameTextView;
//TextView busLineVehicleIcon;
TextView busStopLinesTextView;
TextView busStopLocaLityTextView;
}
public StopAdapter(Context context, List stops) {
super(context, row_layout, stops);
li = LayoutInflater.from(context);
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
ViewHolder vh;
if(convertView == null) {
convertView = li.inflate(row_layout, null);
vh = new ViewHolder();
vh.busStopIDTextView = (TextView) convertView.findViewById(R.id.busStopID);
vh.busStopNameTextView = (TextView) convertView.findViewById(R.id.busStopName);
vh.busStopLinesTextView = (TextView) convertView.findViewById(R.id.routesThatStopHere);
vh.busStopLocaLityTextView = (TextView) convertView.findViewById(R.id.busStopLocality);
convertView.setTag(vh);
} else {
vh = (ViewHolder) convertView.getTag();
}
Stop stop = getItem(position);
vh.busStopIDTextView.setText(stop.ID);
// NOTE: intentionally ignoring stop username in search results: if it's in the favorites, why are you searching for it?
vh.busStopNameTextView.setText(stop.getStopDisplayName());
String whatStopsHere = stop.routesThatStopHereToString();
if(whatStopsHere == null) {
vh.busStopLinesTextView.setVisibility(View.GONE);
} else {
vh.busStopLinesTextView.setText(whatStopsHere);
vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern
}
if(stop.type == null) {
vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
} else {
switch(stop.type) {
case BUS:
default:
vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0);
break;
case METRO:
case RAILWAY:
vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0);
break;
case TRAM:
vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0);
break;
case LONG_DISTANCE_BUS:
// è l'opposto della città ma va beh, dettagli.
vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0);
}
}
if (stop.location == null) {
vh.busStopLocaLityTextView.setVisibility(View.GONE);
} else {
vh.busStopLocaLityTextView.setText(stop.location);
vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern
}
return convertView;
}
}
diff --git a/src/it/reyboz/bustorino/backend/DBStatusManager.java b/src/it/reyboz/bustorino/backend/DBStatusManager.java
index 6eed4b5..127abc4 100644
--- a/src/it/reyboz/bustorino/backend/DBStatusManager.java
+++ b/src/it/reyboz/bustorino/backend/DBStatusManager.java
@@ -1,90 +1,90 @@
/*
BusTO - Backend components
Copyright (C) 2019 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import it.reyboz.bustorino.R;
/**
* Class to handle app status modifications, e.g. database is being updated or not
*/
public class DBStatusManager {
private static String PREFERENCES_NAME;// = "it.reyboz.bustorino.statusPreferences";
private String DB_UPDATING;
private SharedPreferences preferences;
private SharedPreferences.OnSharedPreferenceChangeListener prefListener;
private OnDBUpdateStatusChangeListener dbUpdateListener;
public DBStatusManager(Context context, OnDBUpdateStatusChangeListener listener) {
Context thecon = context.getApplicationContext();
this.preferences = thecon.getSharedPreferences(context.getString(R.string.mainSharedPreferences),Context.MODE_PRIVATE);
DB_UPDATING = context.getString(R.string.databaseUpdatingPref);
PREFERENCES_NAME = context.getString(R.string.mainSharedPreferences);
dbUpdateListener = listener;
//this.prefListeners = new ArrayList<>();
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Log.d("BUSTO-PrefListener", "Changed key " + key + " in the sharedPref");
- if (key.equals(DB_UPDATING)) {
+ if (dbUpdateListener!=null && key.equals(DB_UPDATING)) {
dbUpdateListener.onDBStatusChanged(sharedPreferences.getBoolean(DB_UPDATING, dbUpdateListener.defaultStatusValue()));
}
}
};
}
public boolean isDBUpdating(boolean defaultvalue){
if (preferences == null) //preferences = thecon.getSharedPreferences(PREFERENCES_NAME,Context.MODE_PRIVATE);
{
//This should NOT HAPPEN
Log.e("BUSTO_Pref","Preference reference is null");
return false;
} else {
return preferences.getBoolean(DB_UPDATING,defaultvalue);
}
}
public void registerListener(){
if(prefListener!=null)
preferences.registerOnSharedPreferenceChangeListener(prefListener);
}
public void unregisterListener(){
if(prefListener!=null)
preferences.unregisterOnSharedPreferenceChangeListener(prefListener);
}
public void setDbUpdating(boolean value){
final SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(DB_UPDATING,value);
editor.apply();
}
public interface OnDBUpdateStatusChangeListener {
void onDBStatusChanged(boolean updating);
boolean defaultStatusValue();
}
}
diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
index 87bd066..13d9f4e 100644
--- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
+++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
@@ -1,397 +1,395 @@
/*
BusTO - Backend components
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
public class FiveTAPIFetcher implements ArrivalsFetcher{
private static final String DEBUG_NAME = "FiveTAPIFetcher";
private final Map defaultHeaders = getDefaultHeaders();
final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab"));
@Override
public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) {
//set the date for the request as now
Palina p = new Palina(stopID);
//request parameters
String response = performAPIRequest(QueryType.ARRIVALS,stopID,res);
if(response==null) {
if(res.get()==result.SERVER_ERROR_404) {
Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the hack is not working anymore");
res.set(result.EMPTY_RESULT_SET);
}
return p;
}
try {
List routes = parseArrivalsServerResponse(response, res);
for(Route r: routes){
p.addRoute(r);
}
} catch (JSONException ex){
res.set(result.PARSER_ERROR);
return null;
}
res.set(result.OK);
p.sortRoutes();
return p;
}
@Override
public Passaggio.Source getSourceForFetcher() {
return Passaggio.Source.FiveTAPI;
}
List parseArrivalsServerResponse(String JSONresponse, AtomicReference res) throws JSONException{
ArrayList routes = new ArrayList<>(3);
/*
Slight problem:
"longName": ==> DESCRIPTION
"name": "13N",
"departures": [
{
"arrivalTimeInt": 1272,
"time": "21:12",
"rt": false
}]
"lineType": "URBANO" ==> URBANO can be either bus or tram or METRO
*/
JSONArray arr;
try{
arr = new JSONArray(JSONresponse);
String type;
Route.Type routetype;
for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{
if(response == null || response.length()==0) throw new IllegalArgumentException("Response string is null or void");
ArrayList routes = new ArrayList<>(10);
JSONArray lines =new JSONArray(response);
for(int i=0; i 1) {
String secondo = exploded[exploded.length-2];
if (secondo.contains("festivo")) {
festivo = Route.FestiveInfo.FESTIVO;
} else if (secondo.contains("feriale")) {
festivo = Route.FestiveInfo.FERIALE;
} else if(secondo.contains("lun. - ven")) {
serviceDays = Route.reduced_week;
} else if(secondo.contains("sab - fest.")){
serviceDays = Route.weekend;
festivo = Route.FestiveInfo.FESTIVO;
} else {
/*
Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+
"Couldn't find a the service days\n"+
"Description: "+secondo+","+description
);
*/
}
if(exploded.length>2){
switch (exploded[exploded.length-3].trim()) {
case "bus":
t = Route.Type.BUS;
break;
case "tram":
//never happened, but if it could happen you can get it
t = Route.Type.TRAM;
break;
default:
//nothing
}
}
} else //only one piece
if(description.contains("festivo")){
festivo = Route.FestiveInfo.FESTIVO;
} else if(description.contains("feriale")){
festivo = Route.FestiveInfo.FERIALE;
}
if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM;
if(direction.contains("-")){
//Sometimes the actual filtered direction still remains the full line (including both extremes)
direction = direction.split("-")[1];
}
Route r = new Route(lineName.trim(),direction.trim(),t,new ArrayList<>());
if(serviceDays.length>0) r.serviceDays = serviceDays;
r.festivo = festivo;
r.branchid = branchid;
r.description = description.trim();
r.setStopsList(Arrays.asList(stops.split(",")));
routes.add(r);
}
return routes;
}
public List getDirectionsForStop(String stopID, AtomicReference res) {
String response = performAPIRequest(QueryType.DETAILS,stopID,res);
List routes;
try{
routes = parseDirectionsFromResponse(response);
res.set(result.OK);
} catch (JSONException | IllegalArgumentException e) {
e.printStackTrace();
res.set(result.PARSER_ERROR);
routes = null;
}
return routes;
}
public ArrayList getAllStopsFromGTT(AtomicReference res){
String response = performAPIRequest(QueryType.STOPS_ALL,null,res);
if(response==null) return null;
ArrayList stopslist;
try{
JSONObject responseJSON = new JSONObject(response);
JSONArray stops = responseJSON.getJSONArray("stops");
stopslist = new ArrayList<>(stops.length());
for (int i=0;i getAllLinesFromGTT(AtomicReference res){
String resp = performAPIRequest(QueryType.LINES,null,res);
if(resp==null) {
return null;
}
ArrayList routes = null;
try {
JSONArray lines = new JSONArray(resp);
routes = new ArrayList<>(lines.length());
for(int i = 0; i getDefaultHeaders(){
HashMap param = new HashMap<>();
param.put("Host","www.5t.torino.it");
param.put("Connection","Keep-Alive");
param.put("Accept-Encoding", "gzip");
return param;
}
/**
* Create and perform the network request. This method adds parameters and returns the result
* @param t type of request to be performed
* @param stopID optional parameter, stop ID which you need for passages and branches
* @param res result container
* @return a String which contains the result of the query, to be parsed
*/
@Nullable
public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference res){
URL u;
Map param;
try {
String address = getURLForOperation(t,stopID);
//Log.d(DEBUG_NAME,"The address to query is: "+address);
param = getDefaultHeaders();
u = new URL(address);
} catch (UnsupportedEncodingException |MalformedURLException e) {
e.printStackTrace();
res.set(result.PARSER_ERROR);
return null;
}
String response = networkTools.queryURL(u,res,param);
return response;
}
/**
* Get the right url for the operation you are doing, to be fed into the queryURL method
* @param t type of operation
* @param stopID stop on which you are working on
* @return the Url to go to
* @throws UnsupportedEncodingException if it cannot be converted to utf-8
*/
public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException {
final StringBuilder sb = new StringBuilder();
sb.append("http://www.5t.torino.it/ws2.1/rest/");
if(t!=QueryType.LINES) sb.append("stops/");
switch (t){
case ARRIVALS:
sb.append(URLEncoder.encode(stopID,"utf-8"));
sb.append("/departures");
break;
case DETAILS:
sb.append(URLEncoder.encode(stopID,"utf-8"));
sb.append("/branches/details");
break;
case STOPS_ALL:
sb.append("all");
break;
case STOPS_VERSION:
sb.append("version");
break;
case LINES:
sb.append("lines/all");
break;
}
return sb.toString();
}
public enum QueryType {
ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES
}
}
diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java
index 1152efb..46391da 100644
--- a/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java
+++ b/src/it/reyboz/bustorino/backend/FiveTAPIVolleyRequest.java
@@ -1,131 +1,130 @@
/*
BusTO - Backend components
Copyright (C) 2019 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Log;
import com.android.volley.*;
import com.android.volley.toolbox.HttpHeaderParser;
import org.json.JSONException;
import java.io.UnsupportedEncodingException;
-import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class to handle request with the Volley Library
*/
public class FiveTAPIVolleyRequest extends Request {
private static final String LOG_TAG = "BusTO-FiveTAPIVolleyReq";
private ResponseListener listener;
final private String url,stopID;
final private AtomicReference resultRef;
final private FiveTAPIFetcher fetcher;
final private FiveTAPIFetcher.QueryType type;
private FiveTAPIVolleyRequest(int method, String url, String stopID, FiveTAPIFetcher.QueryType kind,
ResponseListener listener,
@Nullable Response.ErrorListener errorListener) {
super(method, url, errorListener);
this.url = url;
this.resultRef = new AtomicReference<>();
this.fetcher = new FiveTAPIFetcher();
this.listener = listener;
this.stopID = stopID;
this.type = kind;
}
@Nullable
public static FiveTAPIVolleyRequest getNewRequest(FiveTAPIFetcher.QueryType type, String stopID,
ResponseListener listener, @Nullable Response.ErrorListener errorListener){
String url;
try {
url = FiveTAPIFetcher.getURLForOperation(type,stopID);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
Log.e(LOG_TAG,"Cannot get an URL for the operation");
return null;
}
return new FiveTAPIVolleyRequest(Method.GET,url,stopID,type,listener,errorListener);
}
@Override
protected Response parseNetworkResponse(NetworkResponse response) {
if(response.statusCode != 200)
return Response.error(new VolleyError("Response Error Code "+response.statusCode));
final String stringResponse = new String(response.data);
List routeList;
try{
switch (type){
case ARRIVALS:
routeList = fetcher.parseArrivalsServerResponse(stringResponse,resultRef);
break;
case DETAILS:
routeList = fetcher.parseDirectionsFromResponse(stringResponse);
break;
default:
//empty
return Response.error(new VolleyError("Invalid query type"));
}
} catch (JSONException e) {
resultRef.set(Fetcher.result.PARSER_ERROR);
//e.printStackTrace();
Log.w("FivetVolleyParser","JSON Exception in parsing response of: "+url);
return Response.error(new ParseError(response));
}
if(resultRef.get()== Fetcher.result.PARSER_ERROR){
return Response.error(new ParseError(response));
}
final Palina p = new Palina(stopID);
p.setRoutes(routeList);
p.sortRoutes();
return Response.success(p, HttpHeaderParser.parseCacheHeaders(response));
}
@Override
protected void deliverResponse(Palina p) {
listener.onResponse(p,type);
}
@Override
public Map getHeaders() {
return FiveTAPIFetcher.getDefaultHeaders();
}
//from https://stackoverflow.com/questions/21867929/android-how-handle-message-error-from-the-server-using-volley
@Override
protected VolleyError parseNetworkError(VolleyError volleyError){
if(volleyError.networkResponse != null && volleyError.networkResponse.data != null){
volleyError = new NetworkError(volleyError.networkResponse);
}
return volleyError;
}
public interface ResponseListener{
void onResponse(Palina result, FiveTAPIFetcher.QueryType type);
}
//public interface ErrorListener extends Response.ErrorListener{}
}
diff --git a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java
index 248ae39..5c38cad 100644
--- a/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java
+++ b/src/it/reyboz/bustorino/backend/GTTJSONFetcher.java
@@ -1,120 +1,120 @@
/*
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.support.annotation.NonNull;
+import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URL;
import java.net.URLEncoder;
import java.util.concurrent.atomic.AtomicReference;
public class GTTJSONFetcher implements ArrivalsFetcher {
@Override @NonNull
public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) {
URL url;
Palina p = new Palina(stopID);
String routename;
String bacino;
String content;
JSONArray json;
int howManyRoutes, howManyPassaggi, i, j, pos; // il misto inglese-italiano è un po' ridicolo ma tanto vale...
JSONObject thisroute;
JSONArray passaggi;
try {
url = new URL("http://www.gtt.to.it/cms/index.php?option=com_gtt&task=palina.getTransitiOld&palina=" + URLEncoder.encode(stopID, "utf-8") + "&realtime=true");
} catch (Exception e) {
res.set(result.PARSER_ERROR);
return p;
}
content = networkTools.queryURL(url, res);
if(content == null) {
return p;
}
try {
json = new JSONArray(content);
} catch(JSONException e) {
res.set(result.PARSER_ERROR);
return p;
}
try {
// returns [{"PassaggiRT":[],"Passaggi":[]}] for non existing stops!
json.getJSONObject(0).getString("Linea"); // if we can get this, then there's something useful in the array.
} catch(JSONException e) {
res.set(result.EMPTY_RESULT_SET);
return p;
}
howManyRoutes = json.length();
if(howManyRoutes == 0) {
res.set(result.EMPTY_RESULT_SET);
return p;
}
try {
for(i = 0; i < howManyRoutes; i++) {
thisroute = json.getJSONObject(i);
routename = thisroute.getString("Linea");
try {
bacino = thisroute.getString("Bacino");
} catch (JSONException ignored) { // if "Bacino" gets removed...
bacino = "U";
}
pos = p.addRoute(routename, thisroute.getString("Direzione"), FiveTNormalizer.decodeType(routename, bacino));
passaggi = thisroute.getJSONArray("PassaggiRT");
howManyPassaggi = passaggi.length();
for(j = 0; j < howManyPassaggi; j++) {
String mPassaggio = passaggi.getString(j);
if (mPassaggio.contains("__")){
mPassaggio = mPassaggio.replace("_", "");
}
p.addPassaggio(mPassaggio.concat("*"), Passaggio.Source.GTTJSON, pos);
}
passaggi = thisroute.getJSONArray("PassaggiPR"); // now the non-real-time ones
howManyPassaggi = passaggi.length();
for(j = 0; j < howManyPassaggi; j++) {
p.addPassaggio(passaggi.getString(j), Passaggio.Source.GTTJSON, pos);
}
}
} catch (JSONException e) {
res.set(result.PARSER_ERROR);
return p;
}
p.sortRoutes();
res.set(result.OK);
return p;
}
@Override
public Passaggio.Source getSourceForFetcher() {
return Passaggio.Source.GTTJSON;
}
}
diff --git a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java
index a0c79ce..1bde9dd 100644
--- a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java
+++ b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java
@@ -1,191 +1,191 @@
/*
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.support.annotation.NonNull;
+import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class GTTStopsFetcher implements StopsFinderByName {
@Override @NonNull
public List FindByName(String name, AtomicReference res) {
URL url;
// sorting an ArrayList should be faster than a LinkedList and the API is limited to 15 results
List s = new ArrayList<>(15);
List s2 = new ArrayList<>(15);
String fullname;
String content;
String bacino;
String localita;
Route.Type type;
JSONArray json;
int howManyStops, i;
JSONObject thisstop;
if(name.length() < 3) {
res.set(result.QUERY_TOO_SHORT);
return s;
}
try {
url = new URL("http://www.gtt.to.it/cms/components/com_gtt/views/palinejson/view.html.php?term=" + URLEncoder.encode(name, "utf-8"));
} catch (Exception e) {
res.set(result.PARSER_ERROR);
return s;
}
content = networkTools.queryURL(url, res);
if(content == null) {
return s;
}
try {
json = new JSONArray(content);
} catch(JSONException e) {
if(content.contains("[]")) {
// when no results are found, server returns a PHP Warning and an empty array. In case they fix the warning, we're looking for the array.
res.set(result.EMPTY_RESULT_SET);
} else {
res.set(result.PARSER_ERROR);
}
return s;
}
howManyStops = json.length();
if(howManyStops == 0) {
res.set(result.EMPTY_RESULT_SET);
return s;
}
try {
for(i = 0; i < howManyStops; i++) {
thisstop = json.getJSONObject(i);
fullname = thisstop.getString("data");
String ID = thisstop.getString("value");
try {
localita = thisstop.getString("localita");
if(localita.equals("[MISSING]")) {
localita = null;
}
} catch(JSONException e) {
localita = null;
}
/*
if(localita == null || localita.length() == 0) {
localita = db.getLocationFromID(ID);
}
//TODO: find località by ContentProvider
*/
try {
bacino = thisstop.getString("bacino");
} catch (JSONException ignored) {
bacino = "U";
}
if(fullname.startsWith("Metro ")) {
type = Route.Type.METRO;
} else if(fullname.length() >= 6 && fullname.startsWith("S00")) {
type = Route.Type.RAILWAY;
} else if(fullname.startsWith("ST")) {
type = Route.Type.RAILWAY;
} else {
type = FiveTNormalizer.decodeType("", bacino);
}
//TODO: refactor using content provider
s.add(new Stop(fullname, ID, localita, type,null));
}
} catch (JSONException e) {
res.set(result.PARSER_ERROR);
return s;
}
if(s.size() < 1) {
// shouldn't happen but prevents the next part from catching fire
res.set(result.EMPTY_RESULT_SET);
return s;
}
Collections.sort(s);
// the next loop won't work with less than 2 items
if(s.size() < 2) {
res.set(result.OK);
return s;
}
/* There are some duplicate stops returned by this API.
* Long distance buses have stop IDs with 5 digits. Always. They are zero-padded if there
* aren't enough. E.g. stop 631 becomes 00631.
*
* Unfortunately you can't use padded stops to query any API.
* Fortunately, unpadded stops return both normal and long distance bus timetables.
* FiveTNormalizer is already removing padding (there may be some padded stops for which the
* API doesn't return an unpadded equivalent), here we'll remove duplicates by skipping
* padded stops, which also never have a location.
*
* I had to draw a finite state machine on a piece of paper to understand how to implement
* this loop.
*/
for(i = 1; i < howManyStops; ) {
Stop current = s.get(i);
Stop previous = s.get(i-1);
// same stop: let's see which one to keep...
if(current.ID.equals(previous.ID)) {
if(previous.location == null) {
// previous one is useless: discard it, increment
i++;
} else if(current.location == null) {
// this one is useless: add previous and skip one
s2.add(previous);
i += 2;
} else {
// they aren't really identical: to err on the side of caution, keep them both.
s2.add(previous);
i++;
}
} else {
// different: add previous, increment
s2.add(previous);
i++;
}
}
// unless the last one was garbage (i would be howManyStops+1 in that case), add it
if(i == howManyStops) {
s2.add(s.get(i-1));
}
res.set(result.OK);
return s2;
}
}
diff --git a/src/it/reyboz/bustorino/backend/Notifications.java b/src/it/reyboz/bustorino/backend/Notifications.java
new file mode 100644
index 0000000..eddbe9c
--- /dev/null
+++ b/src/it/reyboz/bustorino/backend/Notifications.java
@@ -0,0 +1,46 @@
+package it.reyboz.bustorino.backend;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import it.reyboz.bustorino.R;
+
+public class Notifications {
+ public static final String DEFAULT_CHANNEL_ID ="Default";
+
+ public static void createDefaultNotificationChannel(Context context) {
+ // Create the NotificationChannel, but only on API 26+ because
+ // the NotificationChannel class is new and not in the support library
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ CharSequence name = context.getString(R.string.default_notification_channel);
+ String description = context.getString(R.string.default_notification_channel_description);
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance);
+ channel.setDescription(description);
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ /**
+ * Register a notification channel on Android Oreo and above
+ * @param con a Context
+ * @param name channel name
+ * @param description channel description
+ * @param importance channel importance (from NotificationManager)
+ * @param ID channel ID
+ */
+ public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(ID, name, importance);
+ channel.setDescription(description);
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ NotificationManager notificationManager = con.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+}
diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java
index 6c1e338..16dcb0c 100644
--- a/src/it/reyboz/bustorino/backend/Passaggio.java
+++ b/src/it/reyboz/bustorino/backend/Passaggio.java
@@ -1,159 +1,159 @@
/*
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.support.annotation.NonNull;
+import androidx.annotation.NonNull;
import android.util.Log;
public final class Passaggio implements Comparable {
private static final int UNKNOWN_TIME = -3;
private static final String DEBUG_TAG = "BusTO-Passaggio";
private final String passaggioGTT;
public final int hh,mm;
public final boolean isInRealTime;
public final Source source;
/**
* Useless constructor.
*
* //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
*/
// public Passaggio(@NonNull String TimeGTT) {
// this.passaggio = TimeGTT;
// }
@Override
public String toString() {
return this.passaggioGTT;
}
/**
* Constructs a time (passaggio) for the timetable.
*
* @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
* @throws IllegalArgumentException if nothing reasonable can be extracted from the string
*/
public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) {
passaggioGTT = TimeGTT;
source = sorgente;
String[] parts = TimeGTT.split(":");
String hh,mm;
boolean realtime;
if(parts.length != 2) {
//throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
this.hh = UNKNOWN_TIME;
this.mm = UNKNOWN_TIME;
this.isInRealTime = false;
return;
}
hh = parts[0];
if(parts[1].endsWith("*")) {
mm = parts[1].substring(0, parts[1].length() - 1);
realtime = true;
} else {
mm = parts[1];
realtime = false;
}
int hour=-3,min=-3;
try {
hour = Integer.parseInt(hh);
min = Integer.parseInt(mm);
} catch (NumberFormatException ex){
Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes");
hour = UNKNOWN_TIME;
min = UNKNOWN_TIME;
realtime = false;
} finally {
this.hh = hour;
this.mm = min;
this.isInRealTime = realtime;
}
}
public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){
this.hh = hour;
this.mm = minutes;
this.isInRealTime = realtime;
this.source = sorgente;
//Build the passaggio string
StringBuilder sb = new StringBuilder();
sb.append(hour).append(":").append(minutes);
if(realtime) sb.append("*");
this.passaggioGTT = sb.toString();
}
public static String createPassaggioGTT(String timeInput, boolean realtime){
final String time = timeInput.trim();
if(time.contains("*")){
if(realtime) return time;
else return time.substring(0,time.length()-1);
} else{
if(realtime) return time.concat("*");
else return time;
}
}
@Override
public int compareTo(@NonNull Passaggio other) {
if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME)
return 0;
else {
int diff = this.hh - other.hh;
// an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01)
if (diff > 12) { // untested
diff -= 24;
} else if (diff < -12) {
diff += 24;
}
diff *= 60;
diff += this.mm - other.mm;
// we should take into account if one is in real time and the other isn't, shouldn't we?
if (other.isInRealTime) {
++diff;
}
if (this.isInRealTime) {
--diff;
}
//TODO: separate Realtime and Non-Realtime, especially for the GTTJSONFetcher
return diff;
}
}
//
// @Override
// public String toString() {
// String resultString = (this.hh).concat(":").concat(this.mm);
// if(this.isInRealTime) {
// return resultString.concat("*");
// } else {
// return resultString;
// }
// }
public enum Source{
FiveTAPI,GTTJSON,FiveTScraper, UNDETERMINED
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java
index 56394a4..c093110 100644
--- a/src/it/reyboz/bustorino/backend/Route.java
+++ b/src/it/reyboz/bustorino/backend/Route.java
@@ -1,370 +1,370 @@
/*
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.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
public class Route implements Comparable {
final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY};
final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY};
private final static int BRANCHID_MISSING = -1;
private final String name;
public String destinazione;
public final List passaggi;
//create a copy of the list, so that
private List sortedPassaggi;
public final Type type;
public String description;
//ordered list of stops, from beginning to end of line
private List stopsList = null;
public int branchid = BRANCHID_MISSING;
public int[] serviceDays ={};
//0=>feriale, 1=>festivo -2=>unknown
public FestiveInfo festivo = FestiveInfo.UNKNOWN;
public enum Type { // "long distance" sono gli extraurbani.
BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2);
//TODO: decide to give some special parameter to each field
private int code;
Type(int code){
this.code = code;
}
public int getCode(){
return this.code;
}
@Nullable
public static Type fromCode(int i){
switch (i){
case 1:
return BUS;
case 2:
return LONG_DISTANCE_BUS;
case 3:
return METRO;
case 4:
return RAILWAY;
case 5:
return TRAM;
case -2:
return UNKNOWN;
default:
return null;
}
}
}
public enum FestiveInfo{
FESTIVO(1),FERIALE(0),UNKNOWN(-2);
private int code;
FestiveInfo(int code){
this.code = code;
}
public int getCode() {
return code;
}
public static FestiveInfo fromCode(int i){
switch (i){
case -2:
return UNKNOWN;
case 0:
return FERIALE;
case 1:
return FESTIVO;
default:
return UNKNOWN;
}
}
}
/**
* Constructor.
*
* @param name route ID
* @param destinazione terminus\end of line
* @param type bus, long distance bus, underground, and so on
* @param passaggi timetable, a good choice is an ArrayList of size 6
* @param description the description of the line, usually given by the FiveTAPIFetcher
* @see Palina Palina.addRoute() method
*/
public Route(String name, String destinazione, List passaggi, Type type, String description) {
this.name = name;
this.destinazione = parseDestinazione(destinazione);
this.passaggi = passaggi;
this.type = type;
this.description = description;
}
/**
* Constructor used in GTTJSONFetcher, see above
*/
public Route(String name, String destinazione, Type type, List passaggi) {
this(name,destinazione,passaggi,type,null);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,Type t,String description){
this(name,null,new ArrayList<>(),t,description);
}
/**
* Exactly what it says on the tin.
*
* @return times from the timetable
*/
public List getPassaggi() {
return this.passaggi;
}
public void setStopsList(List stopsList) {
this.stopsList = Collections.unmodifiableList(stopsList);
}
public List getStopsList(){
return this.stopsList;
}
/**
* Adds a time (passaggio) to the timetable for this route
*
* @param TimeGTT time in GTT format (e.g. "11:22*")
*/
public void addPassaggio(String TimeGTT, Passaggio.Source source) {
this.passaggi.add(new Passaggio(TimeGTT, source));
}
//Overloaded
public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
this.passaggi.add(new Passaggio(hour, minutes, realtime, source));
}
public static Route.Type getTypeFromSymbol(String route) {
switch (route) {
case "M":
return Route.Type.METRO;
case "T":
return Route.Type.RAILWAY;
}
// default with case "B"
return Route.Type.BUS;
}
private String parseDestinazione(String direzione){
if(direzione==null) return null;
//trial to add space to the parenthesis
String[] exploded = direzione.split("\\(");
if(exploded.length>1){
StringBuilder sb = new StringBuilder();
sb.append(exploded[0]);
for(int i=1; i arrivals;
int max;
if(sort){
if(sortedPassaggi==null){
sortedPassaggi = new ArrayList<>(passaggi.size());
sortedPassaggi.addAll(passaggi);
Collections.sort(sortedPassaggi);
}
arrivals = sortedPassaggi;
} else arrivals = passaggi;
max = Math.min(start_idx + number, arrivals.size());
for(int j= start_idx; j0){
this.passaggi.addAll(other.passaggi);
}
if(this.destinazione == null && other.destinazione!=null) {
this.destinazione = other.destinazione;
adjusted = true;
}
if(!this.isBranchIdValid() && other.isBranchIdValid()) {
this.branchid = other.branchid;
adjusted = true;
}
if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){
this.festivo = other.festivo;
adjusted = true;
}
if(other.description!=null && (this.description==null ||
(this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) ||
(this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) {
this.description = other.description;
}
return adjusted;
}
}
diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java
index 3f5f795..6ece6cc 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 android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import it.reyboz.bustorino.util.LinesNameSorter;
import java.net.URLEncoder;
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();
}
@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() {
// 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 11bc615..41fc5c7 100644
--- a/src/it/reyboz/bustorino/backend/StopsDBInterface.java
+++ b/src/it/reyboz/bustorino/backend/StopsDBInterface.java
@@ -1,66 +1,66 @@
/*
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.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+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/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java
index 54642c0..da540f7 100644
--- a/src/it/reyboz/bustorino/backend/networkTools.java
+++ b/src/it/reyboz/bustorino/backend/networkTools.java
@@ -1,173 +1,173 @@
/*
BusTO - Arrival times for Turin public transports.
Copyright (C) 2014 Valerio Bozzolan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicReference;
public abstract class networkTools {
static String getDOM(final URL url, final AtomicReference res) {
//Log.d("asyncwget", "Catching URL in background: " + uri[0]);
HttpURLConnection urlConnection;
StringBuilder result = null;
try {
urlConnection = (HttpURLConnection) url.openConnection();
} catch(IOException e) {
res.set(Fetcher.result.SERVER_ERROR);
return null;
}
try {
InputStream in = new BufferedInputStream(
urlConnection.getInputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(in));
result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
} catch (Exception e) {
//Log.e("asyncwget", e.getMessage());
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
if (result == null) {
res.set(Fetcher.result.SERVER_ERROR);
return null;
}
res.set(Fetcher.result.PARSER_ERROR); // will be set to "OK" later, this is a safety net in case StringBuilder returns null, the website returns an HTTP 204 or something like that.
return result.toString();
}
@Nullable
static String queryURL(URL url, AtomicReference res){
return queryURL(url,res,null);
}
@Nullable
static String queryURL(URL url, AtomicReference res, Map headers) {
HttpURLConnection urlConnection;
InputStream in;
String s;
try {
urlConnection = (HttpURLConnection) url.openConnection();
} catch(IOException e) {
res.set(Fetcher.result.SERVER_ERROR); // even when offline, urlConnection works fine. WHY.
return null;
}
// TODO: make this configurable?
urlConnection.setConnectTimeout(5000);
urlConnection.setReadTimeout(10000);
if(headers!= null){
for(String key : headers.keySet()){
urlConnection.setRequestProperty(key,headers.get(key));
}
}
res.set(Fetcher.result.SERVER_ERROR); // will be set to OK later
try {
in = urlConnection.getInputStream();
} catch (Exception e) {
try {
if(urlConnection.getResponseCode()==404)
res.set(Fetcher.result.SERVER_ERROR_404);
} catch (IOException e2) {
e2.printStackTrace();
}
return null;
}
//s = streamToString(in);
try {
final long startTime = System.currentTimeMillis();
s = parseStreamToString(in);
final long endtime = System.currentTimeMillis();
Log.d("NetworkTools-queryURL","reading response took "+(endtime-startTime)+" millisec");
} catch (IOException e) {
e.printStackTrace();
return null;
}
try {
in.close();
} catch(Exception ignored) {}
try {
urlConnection.disconnect();
} catch(Exception ignored) {}
if(s.length() == 0) {
return null;
} else {
return s;
}
}
// https://stackoverflow.com/a/5445161
static String streamToString(InputStream is) {
Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
/**
* New method, maybe faster, to read inputStream
* also see https://stackoverflow.com/a/5445161
* @param is what to read
* @return the String Read
* @throws IOException from the InputStreamReader
*/
static String parseStreamToString(InputStream is) throws IOException{
final int bufferSize = 1024;
final char[] buffer = new char[bufferSize];
final StringBuilder out = new StringBuilder();
InputStreamReader in = new InputStreamReader(is, "UTF-8");
int rsz= in.read(buffer, 0, buffer.length);
while( rsz >0) {
out.append(buffer, 0, rsz);
rsz = in.read(buffer, 0, buffer.length);
}
return out.toString();
}
static int failsafeParseInt(String str) {
try {
return Integer.parseInt(str);
} catch(NumberFormatException e) {
return 0;
}
}
}
diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
index d16f391..5304479 100644
--- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java
@@ -1,393 +1,395 @@
/*
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 android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
+
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.PalinaAdapter;
import it.reyboz.bustorino.backend.ArrivalsFetcher;
import it.reyboz.bustorino.backend.DBStatusManager;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.FiveTAPIFetcher;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.FiveTScraperFetcher;
import it.reyboz.bustorino.backend.GTTJSONFetcher;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.middleware.AppDataProvider;
import it.reyboz.bustorino.middleware.NextGenDB;
import it.reyboz.bustorino.middleware.UserDB;
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 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 requestedNewArrivalTimes = false;
//Views
protected ImageButton addToFavorites;
protected TextView timesSourceTextView;
private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers));
public static ArrivalsFragment newInstance(String stopID){
return newInstance(stopID, null);
}
public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){
ArrivalsFragment fragment = new ArrivalsFragment();
Bundle args = new Bundle();
args.putString(KEY_STOP_ID,stopID);
//parameter for ResultListFragment
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);
//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(!requestedNewArrivalTimes){
rotateFetchers();
timesSourceTextView.setText(R.string.arrival_source_changing);
mListener.createFragmentForStop(stopID);
requestedNewArrivalTimes = true;
return true;
}
return false;
});
timesSourceTextView.setOnClickListener(view -> {
Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT)
.show();
});
//Button
addToFavorites.setClickable(true);
addToFavorites.setOnClickListener(v -> {
// add/remove the stop in the favorites
mListener.toggleLastStopToFavorites();
});
resultsListView.setOnItemClickListener((parent, view, position, id) -> {
String routeName;
Route r = (Route) parent.getItemAtPosition(position);
routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay());
if (routeName == null) {
routeName = r.getNameForDisplay();
}
if (r.destinazione == null || r.destinazione.length() == 0) {
Toast.makeText(getContext(),
getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(),
getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show();
}
});
String displayName = getArguments().getString(STOP_TITLE);
setTextViewMessage(String.format(
getString(R.string.passages), displayName));
String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW);
if (probablemessage != null) {
//Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage);
messageTextView.setText(probablemessage);
messageTextView.setVisibility(View.VISIBLE);
}
return root;
}
@Override
public void onResume() {
super.onResume();
LoaderManager loaderManager = getLoaderManager();
if(stopID!=null){
//refresh the arrivals
if(!justCreated)
mListener.createFragmentForStop(stopID);
else justCreated = false;
//start the loader
if(prefs.isDBUpdating(true)){
prefs.registerListener();
} else {
loaderManager.restartLoader(loaderFavId, getArguments(), this);
}
updateMessage();
}
}
@Override
public void onStart() {
super.onStart();
if (needUpdateOnAttach){
updateFragmentData(null);
}
}
@Nullable
public String getStopID() {
return stopID;
}
/**
* Give the fetchers
* @return the list of the fetchers
*/
public ArrayList getCurrentFetchers(){
ArrayList v = new ArrayList();
for (ArrivalsFetcher fetcher: fetchers){
v.add(fetcher);
}
return v;
}
public Fetcher[] getCurrentFetchersAsArray(){
Fetcher[] arr = new Fetcher[fetchers.size()];
fetchers.toArray(arr);
return arr;
}
private void rotateFetchers(){
Collections.rotate(fetchers, -1);
}
/**
* Update the UI with the new data
* @param p the full Palina
*/
public void updateFragmentData(@Nullable Palina p){
if (p!=null)
lastUpdatedPalina = p;
if (!isAdded()){
//defer update at next show
if (p==null)
Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null");
else needUpdateOnAttach = true;
} else {
final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina);
showArrivalsSources(lastUpdatedPalina);
super.resetListAdapter(adapter);
}
}
/**
* Set the message of the arrival times source
* @param p Palina with the arrival times
*/
protected void showArrivalsSources(Palina p){
final Passaggio.Source source = p.getPassaggiSourceIfAny();
String source_txt;
switch (source){
case GTTJSON:
source_txt = getString(R.string.gttjsonfetcher);
break;
case FiveTAPI:
source_txt = getString(R.string.fivetapifetcher);
break;
case FiveTScraper:
source_txt = getString(R.string.fivetscraper);
break;
case UNDETERMINED:
//Don't show the view
timesSourceTextView.setVisibility(View.GONE);
return;
default:
throw new IllegalStateException("Unexpected value: " + source);
}
final String base_message = getString(R.string.times_source_fmt, source_txt);
timesSourceTextView.setVisibility(View.VISIBLE);
timesSourceTextView.setText(base_message);
requestedNewArrivalTimes = false;
}
@Override
public void setNewListAdapter(ListAdapter adapter) {
throw new UnsupportedOperationException();
}
/**
* Update the message in the fragment
*
* 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
mListener.updateStarIconFromLastBusStop();
}
@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){
data.moveToFirst();
final String probableName = data.getString(colUserName);
if(probableName!=null && !probableName.isEmpty()){
stopName = probableName;
updateMessage();
}
}
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
}
}
diff --git a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java
index 7767716..b81abc4 100644
--- a/src/it/reyboz/bustorino/fragments/CommonScrollListener.java
+++ b/src/it/reyboz/bustorino/fragments/CommonScrollListener.java
@@ -1,85 +1,85 @@
/*
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.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.widget.AbsListView;
import java.lang.ref.WeakReference;
public class CommonScrollListener extends RecyclerView.OnScrollListener implements AbsListView.OnScrollListener{
WeakReference listenerWeakReference;
//enable swipeRefreshLayout when scrolling down or not
boolean enableRefreshLayout;
int lastvisibleitem;
public CommonScrollListener(FragmentListener lis,boolean enableRefreshLayout){
listenerWeakReference = new WeakReference<>(lis);
this.enableRefreshLayout = enableRefreshLayout;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
/*
* This seems to be a totally useless method
*/
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
FragmentListener listener = listenerWeakReference.get();
if(listener==null){
//can't do anything, sorry
Log.i(this.getClass().getName(),"called onScroll but FragmentListener is null");
return;
}
if (firstVisibleItem>=0) {
if (lastvisibleitem < firstVisibleItem) {
Log.i("Busto", "Scrolling DOWN");
listener.showFloatingActionButton(false);
//lastScrollUp = true;
} else if (lastvisibleitem > firstVisibleItem) {
Log.i("Busto", "Scrolling UP");
listener.showFloatingActionButton(true);
//lastScrollUp = false;
}
lastvisibleitem = firstVisibleItem;
}
if(enableRefreshLayout){
boolean enable = false;
if(view != null && view.getChildCount() > 0){
// check if the first item of the list is visible
boolean firstItemVisible = view.getFirstVisiblePosition() == 0;
// check if the top of the first item is visible
boolean topOfFirstItemVisible = view.getChildAt(0).getTop() == 0;
// enabling or disabling the refresh layout
enable = firstItemVisible && topOfFirstItemVisible;
}
listener.enableRefreshLayout(enable);
//Log.d(getString(R.string.list_fragment_debug),"onScroll active, first item visible: "+firstVisibleItem+", refreshlayout enabled: "+enable);
}}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
FragmentListener listener = listenerWeakReference.get();
if(newState!=SCROLL_STATE_IDLE) listener.showFloatingActionButton(false);
else listener.showFloatingActionButton(true);
}
}
diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
index ed48aa1..710dda2 100644
--- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java
+++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
@@ -1,230 +1,235 @@
/*
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.ContentResolver;
import android.content.ContentValues;
import android.database.sqlite.SQLiteException;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.widget.SwipeRefreshLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.util.Log;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.adapters.PalinaAdapter;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Stop;
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 Stop lastSuccessfullySearchedBusStop;
//support for multiple frames
private int primaryFrameLayout,secondaryFrameLayout, swipeRefID;
public static final int NO_FRAME = -3;
private WeakReference lastTaskRef;
private NextGenDB newDBHelper;
private boolean shouldHaltAllActivities=false;
public FragmentHelper(GeneralActivity act, int swipeRefID, int mainFrame) {
this(act,swipeRefID,mainFrame,NO_FRAME);
}
public FragmentHelper(GeneralActivity act, int swipeRefID, int primaryFrameLayout, int secondaryFrameLayout) {
this.act = act;
this.swipeRefID = swipeRefID;
this.primaryFrameLayout = primaryFrameLayout;
this.secondaryFrameLayout = secondaryFrameLayout;
newDBHelper = new NextGenDB(act.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){
boolean sameFragment;
ArrivalsFragment arrivalsFragment;
if(act==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
return;
}
FragmentManager fm = act.getSupportFragmentManager();
if(fm.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame);
sameFragment = arrivalsFragment.isFragmentForTheSameStop(p);
} else
sameFragment = false;
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));
} else {
Log.d("BusTO", "Same bus stop, accessing existing fragment");
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(R.id.resultFrame);
}
// DO NOT CALL `setListAdapter` ever on arrivals fragment
arrivalsFragment.updateFragmentData(p);
act.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){
act.hideKeyboard();
StopListFragment listfragment = StopListFragment.newInstance(query);
attachFragmentToContainer(act.getSupportFragmentManager(),listfragment,false,"search_"+query);
listfragment.setStopList(resultList);
toggleSpinner(false);
}
/**
* Wrapper for toggleSpinner in Activity
* @param on new status of spinner system
*/
public void toggleSpinner(boolean on){
if (act instanceof FragmentListener)
((FragmentListener) act).toggleSpinner(on);
else {
SwipeRefreshLayout srl = (SwipeRefreshLayout) act.findViewById(swipeRefID);
srl.setRefreshing(false);
}
}
/**
* 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
*/
public void attachFragmentToContainer(FragmentManager fm,Fragment fragment, boolean sendToSecondaryFrame, String tag){
FragmentTransaction ft = fm.beginTransaction();
if(sendToSecondaryFrame && secondaryFrameLayout!=NO_FRAME)
ft.replace(secondaryFrameLayout,fragment,tag);
else ft.replace(primaryFrameLayout,fragment,tag);
ft.addToBackStack("state_"+tag);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
ft.commit();
//fm.executePendingTransactions();
}
synchronized public int insertBatchDataInNextGenDB(ContentValues[] valuesArr,String tableName){
if(newDBHelper !=null)
try {
return newDBHelper.insertBatchContent(valuesArr, tableName);
} catch (SQLiteException exc){
Log.w("DB Batch inserting: ","ERROR Inserting the data batch: ",exc.fillInStackTrace());
return -2;
}
else return -1;
}
synchronized public ContentResolver getContentResolver(){
return act.getContentResolver();
}
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:
- act.showMessage(R.string.network_error);
+ act.showToastMessage(R.string.network_error, true);
break;
case SERVER_ERROR:
if (act.isConnected()) {
- act.showMessage(R.string.parsing_error);
+ act.showToastMessage(R.string.parsing_error, true);
} else {
- act.showMessage(R.string.network_error);
+ act.showToastMessage(R.string.network_error, true);
}
case PARSER_ERROR:
default:
- act.showMessage(R.string.internal_error);
+ showShortToast(R.string.internal_error);
break;
case QUERY_TOO_SHORT:
- act.showMessage(R.string.query_too_short);
+ showShortToast(R.string.query_too_short);
break;
case EMPTY_RESULT_SET:
- act.showMessage(R.string.no_bus_stop_have_this_name);
+ showShortToast(R.string.no_bus_stop_have_this_name);
break;
}
}
+ public void showShortToast(int message){
+ if (act!=null)
+ act.showToastMessage(message,true);
+ }
+
}
diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
index 7d902fc..9d55b31 100644
--- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java
@@ -1,648 +1,648 @@
/*
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 android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.support.v4.util.Pair;
-import android.support.v7.preference.PreferenceManager;
-import android.support.v7.widget.AppCompatButton;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
+import androidx.core.util.Pair;
+import androidx.preference.PreferenceManager;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
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.middleware.AppDataProvider;
import it.reyboz.bustorino.middleware.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 FragmentListener 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 = (RecyclerView) root.findViewById(R.id.stopGridRecyclerView);
gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP));
gridRecyclerView.setLayoutManager(gridLayoutManager);
gridRecyclerView.setHasFixedSize(false);
circlingProgressBar = (ProgressBar) root.findViewById(R.id.loadingBar);
flatProgressBar = (ProgressBar) root.findViewById(R.id.horizontalProgressBar);
messageTextView = (TextView) root.findViewById(R.id.messageTextView);
titleTextView = (TextView) root.findViewById(R.id.titleTextView);
switchButton = (AppCompatButton) 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();
});
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(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: NearbyLocationListener","can start loader "+ canStartDBQuery);
+ 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 d796b29..eb34b5e 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 android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
-import android.support.v4.widget.SwipeRefreshLayout;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
-import android.support.design.widget.FloatingActionButton;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.FiveTNormalizer;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.middleware.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 FragmentListener 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.createFragmentForStop(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;
}
@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 = (SwipeRefreshLayout) 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 FragmentListener) {
mListener = (FragmentListener) 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/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java
index ebfa69f..7901612 100644
--- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java
+++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java
@@ -1,26 +1,26 @@
package it.reyboz.bustorino.fragments;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
-import android.support.v7.preference.Preference;
-import android.support.v7.preference.PreferenceFragmentCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
import it.reyboz.bustorino.R;
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = SettingsFragment.class.getName();
SharedPreferences preferences;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences,rootKey);
preferences = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);
}
}
diff --git a/src/it/reyboz/bustorino/fragments/StopListFragment.java b/src/it/reyboz/bustorino/fragments/StopListFragment.java
index 7f6daa2..d05fc4c 100644
--- a/src/it/reyboz/bustorino/fragments/StopListFragment.java
+++ b/src/it/reyboz/bustorino/fragments/StopListFragment.java
@@ -1,145 +1,145 @@
/*
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 android.support.v4.app.LoaderManager;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
import android.util.Log;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.middleware.AppDataProvider;
import it.reyboz.bustorino.middleware.NextGenDB.Contract.StopsTable;
import it.reyboz.bustorino.adapters.StopAdapter;
import java.util.Arrays;
import java.util.List;
public class StopListFragment extends ResultListFragment implements LoaderManager.LoaderCallbacks {
private List stopList;
private StopAdapter mListAdapter;
private static final String[] dataProjection={StopsTable.COL_LINES_STOPPING,StopsTable.COL_PLACE,StopsTable.COL_TYPE,StopsTable.COL_LOCATION};
private static final String KEY_STOP_ID = "stopID";
private static final String WORDS_SEARCHED= "query";
private static final int EXTRA_ID=160;
private String searchedWords;
public StopListFragment(){
//required empty constructor
}
public static StopListFragment newInstance(String searchQuery) {
Bundle args = new Bundle();
//TODO: search stops inside the DB
args.putString(WORDS_SEARCHED,searchQuery);
StopListFragment fragment = new StopListFragment();
args.putSerializable(LIST_TYPE,FragmentKind.STOPS);
fragment.setArguments(args);
return fragment;
}
public void setStopList(List stopList){
this.stopList = stopList;
}
@Override
public void onResume() {
super.onResume();
LoaderManager loaderManager = getLoaderManager();
if(stopList!=null) {
mListAdapter = new StopAdapter(getContext(),stopList);
resetListAdapter(mListAdapter);
for (int i = 0; i < stopList.size(); i++) {
final Bundle b = new Bundle();
b.putString(KEY_STOP_ID, stopList.get(i).ID);
loaderManager.restartLoader(i, b, this);
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
searchedWords = getArguments().getString(WORDS_SEARCHED);
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
//The id will be the position of the element in the list
Uri.Builder builder = new Uri.Builder();
String stopID = args.getString(KEY_STOP_ID);
//Log.d("StopListLoader","Creating loader for stop "+stopID+" in position: "+id);
if(stopID!=null) {
builder.scheme("content").authority(AppDataProvider.AUTHORITY)
.appendPath("stop").appendPath(stopID);
CursorLoader cursorLoader = new CursorLoader(getContext(),builder.build(),dataProjection,null,null,null);
return cursorLoader;
} else return null;
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
//check that we have valid data
if(data==null) return;
final int numRows = data.getCount();
final int elementIdx = loader.getId();
if (numRows==0) {
Log.w(this.getClass().getName(),"No info for stop in position "+elementIdx);
return;
} else if(numRows>1){
Log.d("StopLoading","we have "+numRows+" rows, should only have 1. Taking the first...");
}
final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING);
data.moveToFirst();
Stop stopToModify = stopList.get(elementIdx);
final String linesStopping = data.getString(linesIndex);
stopToModify.setRoutesThatStopHere(Arrays.asList(linesStopping.split(",")));
try {
final String possibleLocation = data.getString(data.getColumnIndexOrThrow(StopsTable.COL_LOCATION));
if (stopToModify.location == null && possibleLocation != null && !possibleLocation.isEmpty() && !possibleLocation.equals("_")) {
stopToModify.location = possibleLocation;
}
if (stopToModify.type == null) {
stopToModify.type = Route.Type.fromCode(data.getInt(data.getColumnIndex(StopsTable.COL_TYPE)));
}
}catch (IllegalArgumentException arg){
if(arg.getMessage().contains("'location' does not exist")) Log.w("StopLoading","stop with no location found");
}
//Log.d("StopListFragmentLoader","Finished parsing data for stop in position "+elementIdx);
mListAdapter.notifyDataSetChanged();
}
@Override
public void onLoaderReset(Loader loader) {
loader.abandon();
}
}
diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
index 6255fe3..12b6b51 100644
--- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
+++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java
@@ -1,321 +1,322 @@
/*
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.database.SQLException;
import android.net.Uri;
import android.os.AsyncTask;
-import android.support.annotation.NonNull;
+
+import androidx.annotation.NonNull;
import android.util.Log;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.fragments.FragmentHelper;
import it.reyboz.bustorino.middleware.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;
public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers) {
RequestType type;
helperRef = new WeakReference<>(fh);
fh.setLastTaskRef(new WeakReference<>(this));
res = new AtomicReference<>();
theFetchers = fetchers;
if (theFetchers.length < 1){
throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!");
}
if (theFetchers[0] instanceof ArrivalsFetcher){
type = RequestType.ARRIVALS;
} else if (theFetchers[0] instanceof StopsFinderByName){
type = RequestType.STOPS;
} else{
type = null;
}
t = type;
}
@Override
protected Object doInBackground(String... params) {
RecursionHelper r = 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,fh,stopID));
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);
break;
case STOPS:
//this should never be a problem
List stopList = (List) o;
if(query!=null && !isCancelled()) {
fh.createFragmentFor(stopList,query);
} 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 String stopID;
private final FragmentHelper fragmentHelper;
public BranchInserter(List routesToInsert,FragmentHelper fh,String stopID) {
this.routesToInsert = routesToInsert;
this.stopID = stopID;
this.fragmentHelper = fh;
}
@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= new_DB_version && !isUpdateCompulsory) {
+ //don't need to update
+ cancelNotification(notificationID);
+ return Result.success(new Data.Builder().
+ putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build());
+ }
+ //start the real update
+ AtomicReference resultAtomicReference = new AtomicReference<>();
+ DatabaseUpdate.setDBUpdatingFlag(con, shPr,true);
+ final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference);
+ DatabaseUpdate.setDBUpdatingFlag(con, shPr,false);
+
+ if (resultUpdate != DatabaseUpdate.Result.DONE){
+ Fetcher.result result = resultAtomicReference.get();
+
+ final Data.Builder dataBuilder = new Data.Builder();
+ switch (resultUpdate){
+ case ERROR_STOPS_DOWNLOAD:
+ dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS);
+ break;
+ case ERROR_LINES_DOWNLOAD:
+ dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES);
+ break;
+ }
+ cancelNotification(notificationID);
+ return Result.failure(dataBuilder.build());
+ }
+ Log.d(DEBUG_TAG, "Update finished successfully!");
+ //update the version in the shared preference
+ final SharedPreferences.Editor editor = shPr.edit();
+ editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version);
+ editor.apply();
+ cancelNotification(notificationID);
+
+ return Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build());
+ }
+
+ public static Constraints getWorkConstraints(){
+ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresCharging(false).build();
+ }
+
+ public static WorkRequest newFirstTimeWorkRequest(){
+ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS)
+ //.setInputData(new Data.Builder().putBoolean())
+ .build();
+ }
+
+
+ private int showNotification(){
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), Notifications.DEFAULT_CHANNEL_ID)
+ .setContentTitle("Libre BusTO - Updating Database")
+ .setProgress(0,0,true)
+ .setPriority(NotificationCompat.PRIORITY_LOW);
+ builder.setSmallIcon(R.drawable.ic_star_filled);
+ final NotificationManagerCompat notifcManager = NotificationManagerCompat.from(getApplicationContext());
+ final int notification_ID = 32198;
+ notifcManager.notify(notification_ID,builder.build());
+
+ return notification_ID;
+ }
+
+ private void cancelNotification(int notificationID){
+ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext());
+
+ notificationManager.cancel(notificationID);
+ }
+}
diff --git a/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java b/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java
new file mode 100644
index 0000000..660ef94
--- /dev/null
+++ b/src/it/reyboz/bustorino/middleware/DatabaseUpdate.java
@@ -0,0 +1,158 @@
+package it.reyboz.bustorino.middleware;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import it.reyboz.bustorino.R;
+import it.reyboz.bustorino.backend.Fetcher;
+import it.reyboz.bustorino.backend.FiveTAPIFetcher;
+import it.reyboz.bustorino.backend.Route;
+import it.reyboz.bustorino.backend.Stop;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static android.content.Context.MODE_PRIVATE;
+
+
+public class DatabaseUpdate {
+
+ public static final String DEBUG_TAG = "BusTO-DBUpdate";
+ public static final int VERSION_UNAVAILABLE = -2;
+ public static final int JSON_PARSING_ERROR = -4;
+
+ public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion";
+
+ enum Result {
+ DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD
+ }
+
+ /**
+ * Request the server the version of the database
+ * @return the version of the DB, or an error code
+ */
+ public static int getNewVersion(){
+ AtomicReference gres = new AtomicReference<>();
+ String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres);
+ if(networkRequest == null){
+ return VERSION_UNAVAILABLE;
+ }
+
+ try {
+ JSONObject resp = new JSONObject(networkRequest);
+ return resp.getInt("id");
+ } catch (JSONException e) {
+ e.printStackTrace();
+ Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest);
+ return JSON_PARSING_ERROR;
+ }
+ }
+ /**
+ * Run the DB Update
+ * @param con a context
+ * @param gres a result reference
+ * @return result of the update
+ */
+ public static Result performDBUpdate(Context con, AtomicReference gres) {
+
+ final FiveTAPIFetcher f = new FiveTAPIFetcher();
+ final ArrayList stops = f.getAllStopsFromGTT(gres);
+ //final ArrayList cpOp = new ArrayList<>();
+
+ if (gres.get() != Fetcher.result.OK) {
+ Log.w(DEBUG_TAG, "Something went wrong downloading");
+ return Result.ERROR_STOPS_DOWNLOAD;
+
+ }
+ // return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database
+ final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext());
+ final SQLiteDatabase db = dbHelp.getWritableDatabase();
+ //Empty the needed tables
+ db.beginTransaction();
+ //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME);
+ //db.delete(LinesTable.TABLE_NAME,null,null);
+
+ //put new data
+ long startTime = System.currentTimeMillis();
+
+ Log.d(DEBUG_TAG, "Inserting " + stops.size() + " stops");
+ for (final Stop s : stops) {
+ final ContentValues cv = new ContentValues();
+
+ cv.put(NextGenDB.Contract.StopsTable.COL_ID, s.ID);
+ cv.put(NextGenDB.Contract.StopsTable.COL_NAME, s.getStopDefaultName());
+ if (s.location != null)
+ cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, s.location);
+ cv.put(NextGenDB.Contract.StopsTable.COL_LAT, s.getLatitude());
+ cv.put(NextGenDB.Contract.StopsTable.COL_LONG, s.getLongitude());
+ if (s.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName());
+ cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString());
+ if (s.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, s.type.getCode());
+
+ //Log.d(DEBUG_TAG,cv.toString());
+ //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build());
+ //valuesArr[i] = cv;
+ db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv);
+
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ long endTime = System.currentTimeMillis();
+ Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s");
+
+ final ArrayList routes = f.getAllLinesFromGTT(gres);
+
+ if (routes == null) {
+ Log.w(DEBUG_TAG, "Something went wrong downloading the lines");
+ dbHelp.close();
+ return Result.ERROR_LINES_DOWNLOAD;
+
+ }
+
+ db.beginTransaction();
+ startTime = System.currentTimeMillis();
+ for (Route r : routes) {
+ final ContentValues cv = new ContentValues();
+ cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName());
+ switch (r.type) {
+ case BUS:
+ cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO");
+ break;
+ case RAILWAY:
+ cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA");
+ break;
+ case LONG_DISTANCE_BUS:
+ cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA");
+ break;
+ }
+ cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description);
+
+ //db.insert(LinesTable.TABLE_NAME,null,cv);
+ int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()});
+ if (rows < 1) { //we haven't changed anything
+ db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv);
+ }
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ endTime = System.currentTimeMillis();
+ Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s");
+ dbHelp.close();
+
+ return Result.DONE;
+ }
+
+ public static boolean setDBUpdatingFlag(Context con, boolean value){
+ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE);
+ return setDBUpdatingFlag(con, shPr, value);
+ }
+ static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){
+ final SharedPreferences.Editor editor = shPr.edit();
+ editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value);
+ return editor.commit();
+ }
+}
diff --git a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
index 5492b47..a5dbd19 100644
--- a/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
+++ b/src/it/reyboz/bustorino/middleware/DatabaseUpdateService.java
@@ -1,278 +1,278 @@
/*
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.app.IntentService;
import android.content.*;
-import android.database.sqlite.SQLiteDatabase;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Log;
-import it.reyboz.bustorino.ActivityMain;
+
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.FiveTAPIFetcher;
-import it.reyboz.bustorino.backend.Route;
-import it.reyboz.bustorino.backend.Stop;
import org.json.JSONException;
import org.json.JSONObject;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReference;
-import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*;
-
/**
* An {@link IntentService} subclass for handling asynchronous task requests in
* a service on a separate handler thread.
*/
public class DatabaseUpdateService extends IntentService {
// IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS
private static final String ACTION_UPDATE = "it.reyboz.bustorino.middleware.action.UPDATE_DB";
private static final String DB_VERSION = "NextGenDB.GTTVersion";
private static final String DEBUG_TAG = "DatabaseService_BusTO";
// TODO: Rename parameters
private static final String TRIAL = "it.reyboz.bustorino.middleware.extra.TRIAL";
private static final String COMPULSORY = "compulsory_update";
private static final int MAX_TRIALS = 5;
private static final int VERSION_UNAIVALABLE = -2;
public DatabaseUpdateService() {
super("DatabaseUpdateService");
}
private boolean isRunning;
private int updateTrial;
/**
* Starts this service to perform action Foo with the given parameters. If
* the service is already performing a task this action will be queued.
*
* @see IntentService
*/
public static void startDBUpdate(Context con, int trial, @Nullable Boolean mustUpdate){
Intent intent = new Intent(con, DatabaseUpdateService.class);
intent.setAction(ACTION_UPDATE);
intent.putExtra(TRIAL,trial);
if(mustUpdate!=null){
intent.putExtra(COMPULSORY,mustUpdate);
}
con.startService(intent);
}
public static void startDBUpdate(Context con) {
startDBUpdate(con, 0, false);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_UPDATE.equals(action)) {
Log.d(DEBUG_TAG,"Started action update");
SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE);
int versionDB = shPr.getInt(DB_VERSION,-1);
final int trial = intent.getIntExtra(TRIAL,-1);
final SharedPreferences.Editor editor = shPr.edit();
updateTrial = trial;
UpdateRequestParams params = new UpdateRequestParams(intent);
int newVersion = getNewVersion(params);
if(newVersion==VERSION_UNAIVALABLE){
//NOTHING LEFT TO DO
return;
}
Log.d(DEBUG_TAG,"newDBVersion: "+newVersion+" oldVersion: "+versionDB);
if(params.mustUpdate || versionDB==-1 || newVersion>versionDB){
Log.d(DEBUG_TAG,"Downloading the bus stops info");
final AtomicReference gres = new AtomicReference<>();
if(!performDBUpdate(gres))
restartDBUpdateifPossible(params,gres);
else {
editor.putInt(DB_VERSION,newVersion);
// BY COMMENTING THIS, THE APP WILL CONTINUOUSLY UPDATE THE DATABASE
editor.apply();
}
} else {
Log.d(DEBUG_TAG,"No update needed");
}
Log.d(DEBUG_TAG,"Finished update");
setDBUpdatingFlag(shPr,false);
}
}
}
private boolean setDBUpdatingFlag(SharedPreferences shPr,boolean value){
final SharedPreferences.Editor editor = shPr.edit();
editor.putBoolean(getString(R.string.databaseUpdatingPref),value);
return editor.commit();
}
private boolean setDBUpdatingFlag(boolean value){
final SharedPreferences shPr = getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE);
return setDBUpdatingFlag(shPr,value);
}
private boolean performDBUpdate(AtomicReference gres){
+ if(!setDBUpdatingFlag(true))
+ return false;
+
+ /*
final FiveTAPIFetcher f = new FiveTAPIFetcher();
final ArrayList stops = f.getAllStopsFromGTT(gres);
//final ArrayList cpOp = new ArrayList<>();
if(gres.get()!= Fetcher.result.OK){
Log.w(DEBUG_TAG,"Something went wrong downloading");
return false;
}
if(!setDBUpdatingFlag(true))
return false; //If the commit to the SharedPreferences didn't succeed, simply stop updating the database
final NextGenDB dbHelp = new NextGenDB(getApplicationContext());
final SQLiteDatabase db = dbHelp.getWritableDatabase();
//Empty the needed tables
db.beginTransaction();
//db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME);
//db.delete(LinesTable.TABLE_NAME,null,null);
//put new data
long startTime = System.currentTimeMillis();
Log.d(DEBUG_TAG,"Inserting "+stops.size()+" stops");
for (final Stop s : stops) {
final ContentValues cv = new ContentValues();
cv.put(StopsTable.COL_ID, s.ID);
cv.put(StopsTable.COL_NAME, s.getStopDefaultName());
if (s.location != null)
cv.put(StopsTable.COL_LOCATION, s.location);
cv.put(StopsTable.COL_LAT, s.getLatitude());
cv.put(StopsTable.COL_LONG, s.getLongitude());
if (s.getAbsurdGTTPlaceName() != null) cv.put(StopsTable.COL_PLACE, s.getAbsurdGTTPlaceName());
cv.put(StopsTable.COL_LINES_STOPPING, s.routesThatStopHereToString());
if (s.type != null) cv.put(StopsTable.COL_TYPE, s.type.getCode());
//Log.d(DEBUG_TAG,cv.toString());
//cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build());
//valuesArr[i] = cv;
db.replace(StopsTable.TABLE_NAME,null,cv);
}
db.setTransactionSuccessful();
db.endTransaction();
long endTime = System.currentTimeMillis();
Log.d(DEBUG_TAG,"Inserting stops took: "+((double) (endTime-startTime)/1000)+" s");
final ArrayList routes = f.getAllLinesFromGTT(gres);
if(routes==null){
Log.w(DEBUG_TAG,"Something went wrong downloading the lines");
dbHelp.close();
return false;
}
db.beginTransaction();
startTime = System.currentTimeMillis();
for (Route r: routes){
final ContentValues cv = new ContentValues();
cv.put(LinesTable.COLUMN_NAME,r.getName());
switch (r.type){
case BUS:
cv.put(LinesTable.COLUMN_TYPE,"URBANO");
break;
case RAILWAY:
cv.put(LinesTable.COLUMN_TYPE,"FERROVIA");
break;
case LONG_DISTANCE_BUS:
cv.put(LinesTable.COLUMN_TYPE,"EXTRA");
break;
}
cv.put(LinesTable.COLUMN_DESCRIPTION,r.description);
//db.insert(LinesTable.TABLE_NAME,null,cv);
int rows = db.update(LinesTable.TABLE_NAME,cv,LinesTable.COLUMN_NAME+" = ?",new String[]{r.getName()});
if(rows<1){ //we haven't changed anything
db.insert(LinesTable.TABLE_NAME,null,cv);
}
}
db.setTransactionSuccessful();
db.endTransaction();
endTime = System.currentTimeMillis();
Log.d(DEBUG_TAG,"Inserting lines took: "+((double) (endTime-startTime)/1000)+" s");
dbHelp.close();
return true;
+
+ */
+ return DatabaseUpdate.performDBUpdate(getApplication(),gres) == DatabaseUpdate.Result.DONE;
}
private int getNewVersion(UpdateRequestParams params){
AtomicReference gres = new AtomicReference<>();
String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres);
if(networkRequest == null){
restartDBUpdateifPossible(params,gres);
return VERSION_UNAIVALABLE;
}
boolean needed;
try {
JSONObject resp = new JSONObject(networkRequest);
return resp.getInt("id");
} catch (JSONException e) {
e.printStackTrace();
Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest);
return -4;
}
}
private void restartDBUpdateifPossible(UpdateRequestParams pars, AtomicReference res){
if (pars.trial permissionDoneRunnables = new HashMap<>();
+ protected HashMap permissionAsked = new HashMap<>();
protected void setOption(String optionName, boolean value) {
SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit();
editor.putBoolean(optionName, value);
editor.commit();
}
protected boolean getOption(String optionName, boolean optDefault) {
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
return preferences.getBoolean(optionName, optDefault);
}
+
+ protected SharedPreferences getMainSharedPreferences(){
+ return getSharedPreferences(getString(R.string.mainSharedPreferences),MODE_PRIVATE);
+ }
public void hideKeyboard() {
View view = getCurrentFocus();
if (view != null) {
((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(view.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}
public boolean isConnected() {
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
- public abstract void showMessage(int messageID);
- public abstract void showKeyboard();
+
+ public void showToastMessage(int messageID, boolean short_lenght) {
+ final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
+ Toast.makeText(getApplicationContext(), messageID, length).show();
+ }
public void assertLocationPermissions() {
if(ContextCompat.checkSelfPermission(getApplicationContext(),Manifest.permission.ACCESS_FINE_LOCATION)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_POSITION);
}
}
+
+ public int askForPermissionIfNeeded(String permission, int requestID){
+
+ if(ContextCompat.checkSelfPermission(getApplicationContext(),permission)==PackageManager.PERMISSION_GRANTED){
+ return PERMISSION_OK;
+ }
+ //need to ask for the permission
+ //consider scenario when we have already asked for permission
+ boolean alreadyAsked = false;
+ Integer num_trials = 0;
+ synchronized (this){
+ if (permissionAsked.containsKey(permission)){
+ num_trials = permissionAsked.get(permission);
+ if (num_trials != null && num_trials > 3)
+ alreadyAsked = true;
+
+ }
+ }
+ Log.d(DEBUG_TAG,"Already asked for permission: "+permission+" -> "+num_trials);
+
+ if(!alreadyAsked){
+ ActivityCompat.requestPermissions(this,new String[]{permission}, requestID);
+ synchronized (this){
+ if (num_trials!=null){
+ permissionAsked.put(permission, num_trials+1);
+ }
+ }
+ return PERMISSION_ASKING;
+ } else {
+
+ return PERMISSION_NEG_CANNOT_ASK;
+ }
+
+ }
+
public void createSnackbar(int ViewID, String message,int duration){
Snackbar.make(findViewById(ViewID),message,duration);
}
+
/*
METHOD THAT MIGHT BE USEFUL LATER
public void assertPermissions(String[] permissions){
ArrayList permissionstoRequest = new ArrayList<>();
for(int i=0;i.
*/
package it.reyboz.bustorino.middleware;
import android.content.ContentValues;
import android.content.Context;
-import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
-import android.support.annotation.Nullable;
+import androidx.annotation.Nullable;
import android.util.Log;
-import it.reyboz.bustorino.R;
+
+import it.reyboz.bustorino.backend.Fetcher;
+import it.reyboz.bustorino.backend.FiveTAPIFetcher;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
+import org.json.JSONException;
+import org.json.JSONObject;
import java.util.*;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import static it.reyboz.bustorino.middleware.NextGenDB.Contract.*;
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 = {
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 " +
StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG +
" >= ? AND "+ StopsTable.COL_LONG + " <= ?";
private Context appContext;
public NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
- /**
- * Lazy initialization singleton getter, thread-safe with double checked locking
- * from https://en.wikipedia.org/wiki/Singleton_pattern
- * @return the instance
- */
- /*
- public static NextGenDB getInstance(Context context){
- if(instance==null){
- synchronized (NextGenDB.class){
- if(instance==null){
- instance = new NextGenDB(context);
- }
- }
- }
- return instance;
- }*/
@Override
public void onCreate(SQLiteDatabase db) {
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));
}
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
return stops;
}
result.close();
db.close();
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/middleware/StopsDB.java b/src/it/reyboz/bustorino/middleware/StopsDB.java
index 4ddb5d8..42e4de6 100644
--- a/src/it/reyboz/bustorino/middleware/StopsDB.java
+++ b/src/it/reyboz/bustorino/middleware/StopsDB.java
@@ -1,308 +1,308 @@
/*
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.middleware;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;
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/util/RoutePositionSorter.java b/src/it/reyboz/bustorino/util/RoutePositionSorter.java
index 52b9766..055d9c5 100644
--- a/src/it/reyboz/bustorino/util/RoutePositionSorter.java
+++ b/src/it/reyboz/bustorino/util/RoutePositionSorter.java
@@ -1,71 +1,71 @@
/*
BusTO (util)
Copyright (C) 2019 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.util;
import android.location.Location;
-import android.support.v4.util.Pair;
+import androidx.core.util.Pair;
import android.util.Log;
-import it.reyboz.bustorino.adapters.ArrivalsStopAdapter;
+
import it.reyboz.bustorino.backend.Passaggio;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class RoutePositionSorter implements Comparator> {
private final Location loc;
private final double minutialmetro = 6.0/100; //v = 5km/h
private final double distancemultiplier = 2./3;
public RoutePositionSorter(Location loc) {
this.loc = loc;
}
@Override
public int compare(Pair pair1, Pair pair2) throws NullPointerException{
int delta = 0;
final Stop stop1 = pair1.first, stop2 = pair2.first;
double dist1 = utils.measuredistanceBetween(loc.getLatitude(),loc.getLongitude(),
stop1.getLatitude(),stop1.getLongitude());
double dist2 = utils.measuredistanceBetween(loc.getLatitude(),loc.getLongitude(),
stop2.getLatitude(),stop2.getLongitude());
final List passaggi1 = pair1.second.passaggi,
passaggi2 = pair2.second.passaggi;
if(passaggi1.size()<=0 || passaggi2.size()<=0){
Log.e("ArrivalsStopAdapter","Cannot compare: No arrivals in one of the stops");
} else {
Collections.sort(passaggi1);
Collections.sort(passaggi2);
int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh;
if(deltaOre>12)
deltaOre -= 24;
else if (deltaOre<-12)
deltaOre += 24;
delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm;
}
delta += (int)((dist1 -dist2)*minutialmetro*distancemultiplier);
return delta;
}
@Override
public boolean equals(Object obj) {
return obj instanceof RoutePositionSorter;
}
}