diff --git a/build.gradle b/build.gradle index 4c434a6..bfb2390 100644 --- a/build.gradle +++ b/build.gradle @@ -1,108 +1,117 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } dependencies { classpath 'com.android.tools.build:gradle:4.0.2' } ext { //libraries versions - fragment_version = "1.3.0" + fragment_version = "1.3.3" activity_version = "1.1.0" appcompat_version = "1.2.0" preference_version = "1.1.1" work_version = "2.5.0" acra_version = "5.7.0" + lifecycle_version = "2.3.1" + arch_version = "2.1.0" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() } } apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion '29.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 14 targetSdkVersion 29 versionCode 32 versionName "1.14.1" vectorDrawables.useSupportLibrary = true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { jcenter() mavenLocal() } dependencies { //new libraries implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.activity:activity:$activity_version" - implementation "androidx.annotation:annotation:1.1.0" + implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation 'com.google.android.material:material:1.3.0' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' - implementation 'com.android.volley:volley:1.1.1' + implementation 'com.android.volley:volley:1.2.0' implementation 'org.osmdroid:osmdroid-android:6.1.8' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.14.0' + // ViewModel + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + // LiveData + implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" + // Lifecycles only (without ViewModel or LiveData) + implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + } } diff --git a/res/layout/fragment_favorites.xml b/res/layout/fragment_favorites.xml new file mode 100644 index 0000000..07ef784 --- /dev/null +++ b/res/layout/fragment_favorites.xml @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityFavorites.java b/src/it/reyboz/bustorino/ActivityFavorites.java index c43cdb0..321f43d 100644 --- a/src/it/reyboz/bustorino/ActivityFavorites.java +++ b/src/it/reyboz/bustorino/ActivityFavorites.java @@ -1,299 +1,296 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; -import android.database.Cursor; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; +import androidx.lifecycle.ViewModelProvider; + import android.widget.*; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.adapters.StopAdapter; -import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; -import it.reyboz.bustorino.data.StopsDB; +import it.reyboz.bustorino.data.FavoritesViewModel; import it.reyboz.bustorino.data.UserDB; +import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; import android.app.AlertDialog; -import android.content.Context; import android.content.DialogInterface; -import android.os.AsyncTask; + import androidx.core.app.NavUtils; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import java.util.List; -public class ActivityFavorites extends AppCompatActivity implements LoaderManager.LoaderCallbacks { +public class ActivityFavorites extends AppCompatActivity { private ListView favoriteListView; private SQLiteDatabase userDB; - private EditText bus_stop_name; + private EditText busStopNameText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_favorites); // this should be done in onStarted and closed in onStop, but apparently onStarted is never run. this.userDB = new UserDB(getApplicationContext()).getWritableDatabase(); ActionBar ab = getSupportActionBar(); assert ab != null; ab.setIcon(R.drawable.ic_launcher); ab.setDisplayHomeAsUpEnabled(true); // Back button favoriteListView = (ListView) findViewById(R.id.favoriteListView); + favoriteListView.setOnItemClickListener((parent, view, position, id) -> { + /** + * Casting because of Javamerda + * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener + */ + Stop busStop = (Stop) parent.getItemAtPosition(position); + + Intent intent = new Intent(ActivityFavorites.this, + ActivityMain.class); + + Bundle b = new Bundle(); + // TODO: is passing a serialized object a good idea? Or rather, is it reasonably fast? + //b.putSerializable("bus-stop-serialized", busStop); + b.putString("bus-stop-ID", busStop.ID); + b.putString("bus-stop-display-name", busStop.getStopDisplayName()); + intent.putExtras(b); + //intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + // Intent.FLAG_ACTIVITY_CLEAR_TASK isn't supported in API < 11 and we're targeting API 7... + intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + + startActivity(intent); + + finish(); + }); + registerForContextMenu(favoriteListView); + + FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); + model.getFavorites().observe(this, this::showStops); - createFavoriteList(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (v.getId() == R.id.favoriteListView) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { // Respond to the action bar's Up/Home button case android.R.id.home: NavUtils.navigateUpFromSameTask(this); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item .getMenuInfo(); Stop busStop = (Stop) favoriteListView.getItemAtPosition(info.position); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: + new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, + new AsyncStopFavoriteAction.ResultListener() { + @Override + public void doStuffWithResult(Boolean result) { - // remove the stop from the favorites in background - new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, new AsyncStopFavoriteAction.ResultListener() { - @Override - public void doStuffWithResult(Boolean result) { - //Update favorites list - createFavoriteList(); - } - }).execute(busStop); + } + }).execute(busStop); return true; + case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: final String theGeoUrl = busStop.getGeoURL(); if(theGeoUrl==null){ //doesn't have a position Toast.makeText(getApplicationContext(),R.string.cannot_show_on_map_no_position,Toast.LENGTH_SHORT).show(); return true; } // start ActivityMap with these extras in intent Intent intent = new Intent(ActivityFavorites.this, ActivityMap.class); Bundle b = new Bundle(); - b.putDouble("lat", busStop.getLatitude()); - b.putDouble("lon", busStop.getLongitude()); + double lat, lon; + if (busStop.getLatitude()!=null) + lat = busStop.getLatitude(); + else lat = 200; + if (busStop.getLongitude()!=null) + lon = busStop.getLongitude(); + else lon = 200; + b.putDouble("lat", lat); + b.putDouble("lon",lon); b.putString("name", busStop.getStopDefaultName()); b.putString("ID", busStop.ID); intent.putExtras(b); startActivity(intent); return true; default: return super.onContextItemSelected(item); } } - void createFavoriteList() { - // TODO: memoize default list, query only user names every time? - new AsyncGetFavorites(getApplicationContext(), this.userDB).execute(); + + void showStops(List busStops){ + // If no data is found show a friendly message + if (busStops.size() == 0) { + favoriteListView.setVisibility(View.INVISIBLE); + TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); + assert favoriteTipTextView != null; + favoriteTipTextView.setVisibility(View.VISIBLE); + ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); + angeryBusImageView.setVisibility(View.VISIBLE); + } + /* There's a nice method called notifyDataSetChanged() to avoid building the ListView + * all over again. This method exists in a billion answers on Stack Overflow, but + * it's nowhere to be seen around here, Android Studio can't find it no matter what. + * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I + * guess) and requires to modify the list with .add() and .clear() and some other + * methods, so to update a single stop we need to completely rebuild the list for no + * reason. It would probably end up as "slow" as throwing away the old ListView and + * redrwaing everything. + */ + + // Show results + favoriteListView.setAdapter(new StopAdapter(this, busStops)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); - bus_stop_name = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); - bus_stop_name.setText(busStop.getStopDisplayName()); - bus_stop_name.setHint(busStop.getStopDefaultName()); + busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); + busStopNameText.setText(busStop.getStopDisplayName()); + busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - String busStopUsername = bus_stop_name.getText().toString(); + String busStopUsername = busStopNameText.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.length() == 0) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); - UserDB.updateStop(busStop, userDB); - createFavoriteList(); + //UserDB.updateStop(busStop, userDB); + //UserDB.notifyContentProvider(getApplicationContext()); } } else { // changed to something // something different? - if(oldUserName == null || !busStopUsername.equals(oldUserName)) { + if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); - UserDB.updateStop(busStop, userDB); - createFavoriteList(); + //UserDB.updateStop(busStop, userDB); + //UserDB.notifyContentProvider(getApplicationContext()); } } + new AsyncStopFavoriteAction(getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, + new AsyncStopFavoriteAction.ResultListener() { + @Override + public void doStuffWithResult(Boolean result) { + //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); + } + }).execute(busStop); + } }); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // delete user name from database busStop.setStopUserName(null); UserDB.updateStop(busStop, userDB); - createFavoriteList(); } }); builder.show(); } /** * This one runs. onStart instead gets ignored for no reason whatsoever. * * @see Android Activity Lifecycle */ @Override protected void onStop() { super.onStop(); this.userDB.close(); } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return null; - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - - } - - @Override - public void onLoaderReset(Loader loader) { - - } - +/* private class AsyncGetFavorites extends AsyncTask> { private Context c; private SQLiteDatabase userDB; AsyncGetFavorites(Context c, SQLiteDatabase userDB) { this.c = c; this.userDB = userDB; } @Override protected List doInBackground(Void... voids) { StopsDB stopsDB = new StopsDB(c); stopsDB.openIfNeeded(); List busStops = UserDB.getFavorites(this.userDB, stopsDB); stopsDB.closeIfNeeded(); return busStops; } @Override protected void onPostExecute(List busStops) { - // If no data is found show a friendly message - if (busStops.size() == 0) { - favoriteListView.setVisibility(View.INVISIBLE); - TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); - assert favoriteTipTextView != null; - favoriteTipTextView.setVisibility(View.VISIBLE); - ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); - angeryBusImageView.setVisibility(View.VISIBLE); - } - - /* There's a nice method called notifyDataSetChanged() to avoid building the ListView - * all over again. This method exists in a billion answers on Stack Overflow, but - * it's nowhere to be seen around here, Android Studio can't find it no matter what. - * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I - * guess) and requires to modify the list with .add() and .clear() and some other - * methods, so to update a single stop we need to completely rebuild the list for no - * reason. It would probably end up as "slow" as throwing away the old ListView and - * redrwaing everything. - */ - // Show results - favoriteListView.setAdapter(new StopAdapter(this.c, busStops)); - favoriteListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int position, long id) { - /** - * Casting because of Javamerda - * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener - */ - Stop busStop = (Stop) parent.getItemAtPosition(position); - - Intent intent = new Intent(ActivityFavorites.this, - ActivityMain.class); - - Bundle b = new Bundle(); - // TODO: is passing a serialized object a good idea? Or rather, is it reasonably fast? - //b.putSerializable("bus-stop-serialized", busStop); - b.putString("bus-stop-ID", busStop.ID); - b.putString("bus-stop-display-name", busStop.getStopDisplayName()); - intent.putExtras(b); - //intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - // Intent.FLAG_ACTIVITY_CLEAR_TASK isn't supported in API < 11 and we're targeting API 7... - intent.setFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); - - startActivity(intent); - - finish(); - } - }); - registerForContextMenu(favoriteListView); } } +*/ + } diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 568e345..5d26f66 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,840 +1,838 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; -import android.database.sqlite.SQLiteDatabase; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.preference.PreferenceManager; import androidx.work.BackoffPolicy; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.snackbar.Snackbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.core.app.NavUtils; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.*; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; -import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; import it.reyboz.bustorino.util.Permissions; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; public class ActivityMain extends GeneralActivity implements FragmentListenerMain { /* * Layout elements */ private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem, experimentsMenuItem; private SwipeRefreshLayout swipeRefreshLayout; private FloatingActionButton floatingActionButton; private FragmentManager framan; private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; private ImageButton addToFavorites; /* * Options */ private final String OPTION_SHOW_LEGEND = "show_legend"; private static final String DEBUG_TAG = "BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference res) { SystemClock.sleep(5000); res.set(result.SERVER_ERROR); return new Palina(); } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ private ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; private StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; /* * Position */ //Fine location criteria private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// /* * @see swipeRefreshLayout */ private final Handler theHandler = new Handler(); private final Runnable refreshStop = new Runnable() { public void run() { if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment == null){ new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fh, fragment.getCurrentFetchersAsArray(), getApplicationContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(fh, arrivalsFetchers, getApplicationContext()).execute(); } }; //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); final SharedPreferences theShPr = getMainSharedPreferences(); /* * UI */ setContentView(R.layout.activity_main); Toolbar defToolbar = findViewById(R.id.that_toolbar); setSupportActionBar(defToolbar); busStopSearchByIDEditText = findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = findViewById(R.id.busStopSearchByNameEditText); progressBar = findViewById(R.id.progressBar); howDoesItWorkTextView = findViewById(R.id.howDoesItWorkTextView); hideHintButton = findViewById(R.id.hideHintButton); swipeRefreshLayout = findViewById(R.id.listRefreshLayout); floatingActionButton = findViewById(R.id.floatingActionButton); framan.addOnBackStackChangedListener(() -> Log.d("MainActivity, BusTO", "BACK STACK CHANGED")); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); // Called when the layout is pulled down swipeRefreshLayout .setOnRefreshListener(() -> theHandler.post(refreshStop)); /** * @author Marco Gagino!!! */ //swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); fh = new FragmentHelper(this, framan, getApplicationContext(),R.id.resultFrame); setSearchModeBusStopID(); //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; String busStopDisplayName = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); busStopDisplayName = b.getString("bus-stop-display-name"); /** * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully setBusStopSearchByIDEditText(busStopID); /* //THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS // STOP IS ADDED AUTOMATICALLY Stop nextStop = new Stop(busStopID); // forcing it as user name even though it could be standard name, it doesn't really matter nextStop.setStopUserName(busStopDisplayName); //set stop as last succe fh.setLastSuccessfullySearchedBusStop(nextStop); */ requestArrivalsForStopID(busStopID); } //Try (hopefully) database update PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 1, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); final WorkManager workManager = WorkManager.getInstance(this); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); if (version >= 0) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); /* Set database update */ workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// //SETUP LOCATION locmgr = (LocationManager) getSystemService(LOCATION_SERVICE); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); //We want the nearby bus stops! theHandler.post(new NearbyStopsRequester(this)); //If there are no providers available, then, wait for them Log.d("MainActivity", "Created"); } /* * Reload bus stop timetable when it's fulled resumed from background. * @Override protected void onPostResume() { * super.onPostResume(); * Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop()); * if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) { * setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID); * new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); * } else { * //we have new activity or we don't have a new searched stop. * //Let's search stops nearby * LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); * Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.resultFrame); *

*

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

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