diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -12,13 +12,15 @@ } 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" } } @@ -82,7 +84,7 @@ //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" @@ -95,7 +97,7 @@ 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 @@ -104,5 +106,12 @@ // 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 --- /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 --- a/src/it/reyboz/bustorino/ActivityFavorites.java +++ b/src/it/reyboz/bustorino/ActivityFavorites.java @@ -17,20 +17,18 @@ */ 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; @@ -47,10 +45,10 @@ 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) { @@ -66,8 +64,35 @@ 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 @@ -100,17 +125,16 @@ 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; @@ -125,8 +149,15 @@ // 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); @@ -138,9 +169,29 @@ } } - 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) { @@ -149,16 +200,16 @@ 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 @@ -166,17 +217,25 @@ // 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() { @@ -192,7 +251,6 @@ busStop.setStopUserName(null); UserDB.updateStop(busStop, userDB); - createFavoriteList(); } }); builder.show(); @@ -208,22 +266,7 @@ 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; @@ -245,55 +288,9 @@ @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 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -24,7 +24,6 @@ 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; @@ -67,7 +66,6 @@ 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; diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -34,6 +34,7 @@ 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; @@ -48,8 +49,11 @@ 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) { @@ -191,6 +195,20 @@ 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()); @@ -239,12 +257,9 @@ 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)) { @@ -263,7 +278,11 @@ 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(); @@ -279,7 +298,7 @@ snackbar.show(); } - private MainScreenFragment showMainFragment(){ + private MainScreenFragment createAndShowMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); MainScreenFragment fragment = MainScreenFragment.newInstance(); @@ -289,6 +308,44 @@ 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(); @@ -297,6 +354,7 @@ else return null; } + @Override public void showFloatingActionButton(boolean yes) { //TODO @@ -312,24 +370,26 @@ @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 diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -96,7 +96,7 @@ } // no string yet? build it! - return buildString(); + return buildRoutesString(); } @Nullable @@ -117,7 +117,7 @@ return routesThatStopHere; } - private @Nullable String buildString() { + private @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; diff --git a/src/it/reyboz/bustorino/backend/StopsDBInterface.java b/src/it/reyboz/bustorino/backend/StopsDBInterface.java --- a/src/it/reyboz/bustorino/backend/StopsDBInterface.java +++ b/src/it/reyboz/bustorino/backend/StopsDBInterface.java @@ -36,15 +36,6 @@ */ @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... diff --git a/src/it/reyboz/bustorino/data/AppDataProvider.java b/src/it/reyboz/bustorino/data/AppDataProvider.java --- a/src/it/reyboz/bustorino/data/AppDataProvider.java +++ b/src/it/reyboz/bustorino/data/AppDataProvider.java @@ -26,10 +26,13 @@ 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"; @@ -42,6 +45,9 @@ 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; @@ -74,6 +80,7 @@ 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) { @@ -231,7 +238,7 @@ } 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); @@ -241,8 +248,15 @@ 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"); diff --git a/src/it/reyboz/bustorino/data/CustomAsyncQueryHandler.java b/src/it/reyboz/bustorino/data/CustomAsyncQueryHandler.java new file mode 100644 --- /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 --- /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 --- /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 --- a/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/src/it/reyboz/bustorino/data/NextGenDB.java @@ -76,16 +76,18 @@ 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); @@ -164,9 +166,6 @@ String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); - String[] queryColumns = {}; - String stopID; - Route.Type type; if(db == null) { return stops; @@ -176,34 +175,7 @@ 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"); @@ -217,6 +189,42 @@ 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 diff --git a/src/it/reyboz/bustorino/data/StopsDB.java b/src/it/reyboz/bustorino/data/StopsDB.java --- a/src/it/reyboz/bustorino/data/StopsDB.java +++ b/src/it/reyboz/bustorino/data/StopsDB.java @@ -121,35 +121,6 @@ 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; diff --git a/src/it/reyboz/bustorino/data/UserDB.java b/src/it/reyboz/bustorino/data/UserDB.java --- a/src/it/reyboz/bustorino/data/UserDB.java +++ b/src/it/reyboz/bustorino/data/UserDB.java @@ -24,9 +24,11 @@ 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; @@ -41,7 +43,11 @@ 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; } @@ -224,7 +230,6 @@ l.add(s); } } - c.close(); } catch(SQLiteException ignored) {} @@ -233,6 +238,31 @@ 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(); diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -65,7 +65,8 @@ 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()}; @@ -86,6 +87,7 @@ private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); + private boolean reloadOnResume = true; public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); @@ -95,7 +97,7 @@ 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); @@ -108,6 +110,8 @@ 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; @@ -198,22 +202,26 @@ 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(); @@ -221,12 +229,30 @@ 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 @@ -376,11 +402,10 @@ 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; } @@ -406,13 +431,6 @@ } - @Override - public void onPause() { - if(listener!=null) - prefs.unregisterListener(); - super.onPause(); - } - @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java new file mode 100644 --- /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 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -19,6 +19,7 @@ import android.content.Context; + import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -30,7 +31,6 @@ 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; @@ -87,7 +87,7 @@ * 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; @@ -102,6 +102,7 @@ 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; @@ -120,13 +121,16 @@ } 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); @@ -137,7 +141,7 @@ * @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) { @@ -145,7 +149,8 @@ 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); @@ -163,15 +168,22 @@ * 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(); @@ -229,4 +241,27 @@ 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 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -12,6 +12,7 @@ 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; @@ -37,7 +38,6 @@ 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; @@ -45,9 +45,7 @@ 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; @@ -63,7 +61,8 @@ 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"; @@ -82,6 +81,7 @@ private FloatingActionButton floatingActionButton; private boolean setupOnAttached = true; + private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode @@ -124,6 +124,8 @@ //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; + private String pendingStopID = null; + public MainScreenFragment() { // Required empty public constructor } @@ -207,11 +209,59 @@ 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; @@ -219,12 +269,17 @@ 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 { } } @@ -232,6 +287,7 @@ public void onDetach() { super.onDetach(); mListener = null; + // setupOnAttached = true; } @@ -245,6 +301,23 @@ 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 @@ -311,6 +384,8 @@ ////////////////////////////////////// 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); @@ -423,9 +498,24 @@ } + /** + * 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); @@ -442,7 +532,7 @@ } 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 ////////// diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -165,6 +165,7 @@ switchButton.setOnClickListener(v -> { switchFragmentType(); }); + Log.d(DEBUG_TAG, "onCreateView"); return root; } @@ -224,6 +225,7 @@ throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } + Log.d(DEBUG_TAG, "OnAttach called"); } @@ -271,12 +273,12 @@ MIN_NUM_STOPS = Integer.parseInt(shpr.getString(getString(R.string.pref_key_num_recents),"12")); } + @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); gridRecyclerView.setVisibility(View.INVISIBLE); gridRecyclerView.addOnScrollListener(scrollListener); - } @Override diff --git a/src/it/reyboz/bustorino/fragments/ResultListFragment.java b/src/it/reyboz/bustorino/fragments/ResultListFragment.java --- a/src/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/src/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -190,7 +190,7 @@ } public static String getFragmentTag(Palina p) { - return p.ID; + return "palina_"+p.ID; } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -54,6 +54,7 @@ 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) { @@ -62,6 +63,7 @@ fh.setLastTaskRef(new WeakReference<>(this)); res = new AtomicReference<>(); this.context = context.getApplicationContext(); + this.replaceFragment = true; theFetchers = fetchers; if (theFetchers.length < 1){ @@ -206,13 +208,13 @@ 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: diff --git a/src/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java b/src/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java --- a/src/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java +++ b/src/it/reyboz/bustorino/middleware/AsyncStopFavoriteAction.java @@ -20,10 +20,12 @@ 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; /** @@ -31,11 +33,13 @@ */ 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 @@ -86,6 +90,9 @@ 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); @@ -108,11 +115,12 @@ 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(); }