commit b384f305d3becc89056cce5f17fa28cc10e2d927 Author: Fabio Mazza Date: Wed Sep 30 14:50:25 2026 +0200 Show stop suggestions from the database Summary: Search directly in the top bar the stops, either by id or by name. Remove the search button, it is going to be replaced by the search action with the keyboard Fixes T1442 Test Plan: Test that the suggestions work: - when searching ridiculous numbers or absurd names, there is a message text - when clicking on a stop, the message disappears and the search too - the floating action button never disappears Also check nearby fragments (stops) for regression issues. Reviewers: #libre_busto_hackers, valerio.bozzolan Reviewed By: #libre_busto_hackers, valerio.bozzolan Subscribers: valerio.bozzolan Project Tags: #libre_busto Maniphest Tasks: T1442 Differential Revision: https://gitpull.it/D259 diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/OnStopClickListener.kt b/app/src/main/java/it/reyboz/bustorino/adapters/OnStopClickListener.kt new file mode 100644 index 0000000..eab4c31 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/adapters/OnStopClickListener.kt @@ -0,0 +1,7 @@ +package it.reyboz.bustorino.adapters + +import it.reyboz.bustorino.backend.Stop + +fun interface OnStopClickListener { + fun onStopClick(stop: Stop) +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/StopNearbyAdapter.java similarity index 73% rename from app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java rename to app/src/main/java/it/reyboz/bustorino/adapters/StopNearbyAdapter.java index 97c480d..2fe7464 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/StopNearbyAdapter.java @@ -31,21 +31,25 @@ import it.reyboz.bustorino.backend.GPSPoint; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.util.StopSorterByDistance; import it.reyboz.bustorino.fragments.FragmentListenerMain; +import org.jetbrains.annotations.NotNull; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; -public class SquareStopAdapter extends RecyclerView.Adapter { +public class StopNearbyAdapter extends RecyclerView.Adapter { private final static int layoutRes = R.layout.item_stop_nearby_card; //private List stops; private @Nullable GPSPoint userPosition; - private FragmentListenerMain listener; - private List stops; + private OnStopClickListener listener; + private boolean showLocation; + private ArrayList stops; - public SquareStopAdapter(@Nullable List stopList, FragmentListenerMain fragmentListener, @Nullable GPSPoint pos) { + public StopNearbyAdapter(@NotNull ArrayList stopList, + @Nullable GPSPoint pos, boolean showLocation, OnStopClickListener fragmentListener) { listener = fragmentListener; userPosition = pos; stops = stopList; + this.showLocation = showLocation; } @@ -54,21 +58,24 @@ public class SquareStopAdapter extends RecyclerView.Adapter 0) - Collections.sort(stops,new StopSorterByDistance(userPosition)); + if (userPosition!=null) { + if (stops != null && stops.size() > 0) + stops.sort(new StopSorterByDistance(userPosition)); + } return new SquareViewHolder(view); } @Override public void onBindViewHolder(SquareViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA - if(stops==null || stops.size() == 0) return; //NO STOPS + if(stops==null || stops.isEmpty()) return; //NO STOPS final Stop stop = stops.get(position); final Context context = holder.itemView.getContext(); if(stop!=null){ - if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ + if(userPosition!=null && stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ Double distance = stop.getDistanceFromLocation(userPosition); holder.distancetextView.setText(distance.intValue()+" m"); + holder.distancetextView.setVisibility(View.VISIBLE); } else { holder.distancetextView.setVisibility(View.GONE); } @@ -80,10 +87,19 @@ public class SquareStopAdapter extends RecyclerView.Adapter {listener.onStopClick(stop);}); + if (showLocation && !( + stop.location==null || stop.location.isEmpty() || stop.location.contains("null")) + ){ + holder.locationTextView.setText(stop.location); + holder.locationTextView.setVisibility(View.VISIBLE); + } else { + holder.locationTextView.setVisibility(View.GONE); + } } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } @@ -94,27 +110,31 @@ public class SquareStopAdapter extends RecyclerView.Adapter newStops) { diff --git a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java index f5a408a..2d7d504 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java +++ b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java @@ -337,6 +337,28 @@ public class NextGenDB extends SQLiteOpenHelper{ }); } + @NonNull + public synchronized List searchStopsByCodeOrName(String query, boolean searchById){ + String escaped = query.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_"); + String sql; + final String like = "%" + escaped + "%"; + if (searchById){ + sql = "SELECT * FROM "+StopsTable.TABLE_NAME+" WHERE "+StopsTable.COL_ID+" LIKE ? ESCAPE '\\'"; + } else{ + sql = "SELECT * FROM "+StopsTable.TABLE_NAME+" WHERE "+StopsTable.COL_NAME+" LIKE ? ESCAPE '\\'"; + } + final List out = new ArrayList<>(); + SQLiteDatabase db = getReadableDatabase(); + try (Cursor c= db.rawQuery(sql, new String[]{like} + ) ) { + out.addAll(getStopsFromCursorAllFields(c)); + } + + return out; + } + /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index f8d3020..00af54b 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -22,18 +22,23 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; +import android.text.InputType; +import android.widget.TextView; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; +import androidx.appcompat.widget.SearchView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; @@ -48,15 +53,18 @@ import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; +import it.reyboz.bustorino.adapters.StopNearbyAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.util.Permissions; import it.reyboz.bustorino.viewmodels.IntroViewModel; +import it.reyboz.bustorino.viewmodels.SearchStopsDBViewModel; import org.jetbrains.annotations.NotNull; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; @@ -81,7 +89,7 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList public final static String FRAGMENT_TAG = "MainScreenFragment"; private enum SearchMode {SEARCH_ID,SEARCH_NAME,INITIAL} - public enum InternalScreen { + public enum InitialScreen { HOME_BUTTONS(0), NEARBY_STOPS(1), ARRIVALS(2), @@ -89,21 +97,21 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList NEARBY_ARRIVALS(4); public final int code; - InternalScreen(int code) { this.code = code; } + InitialScreen(int code) { this.code = code; } @Nullable - public static InternalScreen fromCode(int code) { - for (InternalScreen c : values()) if (c.code == code) return c; + public static InitialScreen fromCode(int code) { + for (InitialScreen c : values()) if (c.code == code) return c; return null; } @NonNull - public static InternalScreen fromFragmentKind(@NonNull FragmentKind kind){ + public static InitialScreen fromFragmentKind(@NonNull FragmentKind kind){ switch (kind){ - case HOME_BUTTONS -> { return InternalScreen.HOME_BUTTONS; } - case NEARBY_STOPS -> { return InternalScreen.NEARBY_STOPS; } - case FragmentKind.ARRIVALS -> { return InternalScreen.ARRIVALS; } - case FragmentKind.STOPS -> { return InternalScreen.STOP_SEARCH; } - case FragmentKind.NEARBY_ARRIVALS -> { return InternalScreen.NEARBY_ARRIVALS; } + case HOME_BUTTONS -> { return InitialScreen.HOME_BUTTONS; } + case NEARBY_STOPS -> { return InitialScreen.NEARBY_STOPS; } + case FragmentKind.ARRIVALS -> { return InitialScreen.ARRIVALS; } + case FragmentKind.STOPS -> { return InitialScreen.STOP_SEARCH; } + case FragmentKind.NEARBY_ARRIVALS -> { return InitialScreen.NEARBY_ARRIVALS; } default -> { throw new IllegalArgumentException("Unknown fragment kind"); } @@ -113,13 +121,14 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; - private EditText busStopSearchByIDEditText; - private EditText busStopSearchByNameEditText; + //private EditText busStopSearchByIDEditText; + //private EditText busStopSearchByNameEditText; + + private SearchView busStopSearchView; private ProgressBar progressBar; private FloatingActionButton floatingActionButton; - /// VIEW MODELS in BaseFragment - + private SearchStopsDBViewModel searchStopsViewModel; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; @@ -138,12 +147,23 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList private String pendingStopID = null; private String pendingSearchQuery = null; - private InternalScreen internalScreen = InternalScreen.HOME_BUTTONS; + private InitialScreen initialScreen = InitialScreen.HOME_BUTTONS; private CoordinatorLayout coordLayout; + private RecyclerView suggestionsRecyclerView; + private StopNearbyAdapter suggestionsAdapter = null; + private TextView stopSearchMessageTextView; + //this is really a hackish thing, but it works private final LinkedBlockingQueue thingsToDoOnStart = new LinkedBlockingQueue<>(); + private boolean isSearchModeById(){ + return searchMode != SearchMode.SEARCH_NAME; + } + + private void emptySearchQueryViewModel(){ + searchStopsViewModel.searchStops("",isSearchModeById()); + } private void refreshStop() { if(getContext() == null){ @@ -203,14 +223,14 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } - public static MainScreenFragment newInstance(@NonNull InternalScreen kind, + public static MainScreenFragment newInstance(@NonNull InitialScreen kind, @Nullable String stopId, @Nullable String query) { MainScreenFragment f = new MainScreenFragment(); f.setArguments(makeArgs(kind, stopId, query)); return f; } - public static MainScreenFragment newInstance(@NonNull InternalScreen kind, @Nullable Bundle args){ + public static MainScreenFragment newInstance(@NonNull InitialScreen kind, @Nullable Bundle args){ MainScreenFragment f = new MainScreenFragment(); if (args != null) { f.setArguments(args); @@ -225,7 +245,7 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList * @param query * @return */ - public static Bundle makeArgs(@NonNull InternalScreen kind, @Nullable String stopId, @Nullable String query) { + public static Bundle makeArgs(@NonNull InitialScreen kind, @Nullable String stopId, @Nullable String query) { Bundle b = new Bundle(); b.putInt(ARG_INITIAL_CONTENT, kind.code); if (stopId != null) b.putString(ARG_STOP_ID, stopId); @@ -233,16 +253,16 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList return b; } public static Bundle makeArgsArrivals(@NonNull String stopID){ - return makeArgs(InternalScreen.ARRIVALS, stopID, null); + return makeArgs(InitialScreen.ARRIVALS, stopID, null); } public static Bundle makeArgsStops(@NonNull String query){ - return makeArgs(InternalScreen.STOP_SEARCH, query, null); + return makeArgs(InitialScreen.STOP_SEARCH, query, null); } public static Bundle makeArgsNearby(){ - return makeArgs(InternalScreen.NEARBY_STOPS, null, null); + return makeArgs(InitialScreen.NEARBY_STOPS, null, null); } public static Bundle makeArgsButtonsScreen(){ - return makeArgs(InternalScreen.HOME_BUTTONS, null, null); + return makeArgs(InitialScreen.HOME_BUTTONS, null, null); } @@ -254,9 +274,9 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+ args); if (args.containsKey(ARG_INITIAL_CONTENT)) { - int code = args.getInt(ARG_INITIAL_CONTENT, InternalScreen.HOME_BUTTONS.code); - InternalScreen parsed = InternalScreen.fromCode(code); - internalScreen = (parsed != null) ? parsed : InternalScreen.HOME_BUTTONS; + int code = args.getInt(ARG_INITIAL_CONTENT, InitialScreen.HOME_BUTTONS.code); + InitialScreen parsed = InitialScreen.fromCode(code); + initialScreen = (parsed != null) ? parsed : InitialScreen.HOME_BUTTONS; } String stopId = args.getString(ARG_STOP_ID); if (stopId != null) @@ -266,7 +286,6 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); - } @Override @@ -285,14 +304,31 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // - busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); - busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); + //busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); + //busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); + busStopSearchView = root.findViewById(R.id.busStopSearchView); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); - busStopSearchByIDEditText.setSelectAllOnFocus(true); - busStopSearchByIDEditText + stopSearchMessageTextView = root.findViewById(R.id.stopSearchMessageTextView); + suggestionsRecyclerView = root.findViewById(R.id.suggestionsRecyclerView); + suggestionsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(),LinearLayoutManager.VERTICAL, false)); + EditText searchEditText = busStopSearchView.findViewById(androidx.appcompat.R.id.search_src_text); + searchEditText.setSelectAllOnFocus(true); + searchEditText.setOnFocusChangeListener( (view,hasFocus) ->{ + if(hasFocus){ + showFloatingActionButton(true); + } + }); + + busStopSearchView.setOnFocusChangeListener( (view, hasFocus) ->{ + if(hasFocus){ + showFloatingActionButton(true); + } + }); + //busStopSearchView.setSelectAllOnFocus(true); + /*busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { @@ -301,7 +337,9 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } return false; }); - busStopSearchByNameEditText + + */ + /*searchEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { @@ -311,6 +349,24 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList return false; }); + */ + + busStopSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String q) { + onSearchClick(null); + emptySearchQueryViewModel(); + return true; + } + + @Override + public boolean onQueryTextChange(String q) { + //Log.d(DEBUG_TAG, "Changed query text"); + searchStopsViewModel.searchStops(q, isSearchModeById()); + return true; + } + }); + swipeRefreshLayout .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); @@ -319,7 +375,7 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList floatingActionButton.setImageResource(R.drawable.magnifying_glass_larger); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); - busStopSearchByIDEditText.setOnFocusChangeListener((v, hasFocus) -> { + /*busStopSearchByIDEditText.setOnFocusChangeListener((v, hasFocus) -> { //Log.d(DEBUG_TAG, "stop search by ID has focus: " + hasFocus); if(hasFocus) setSearchModeBusStopID(); @@ -331,6 +387,8 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList setSearchModeBusStopName(); }); + */ + AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); @@ -379,18 +437,14 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList @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, "Saved instance state is: "+savedInstanceState); - //Restore instance state - /*if (savedInstanceState!=null){ - Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); - if (fragment!=null){ - getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); - setupOnStart = false; - } - } + Log.d(DEBUG_TAG, "onViewCreated, have savedInstanceState: "+ (savedInstanceState!=null) + + "; SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); + //Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); + searchStopsViewModel = new ViewModelProvider(requireActivity()).get(SearchStopsDBViewModel.class); + + searchStopsViewModel.getFilteredStopsSearch().observe(getViewLifecycleOwner(), + stops -> setSuggestionsStopRecyclerView(stops, true)); - */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); // The child FragmentManager has restored its content — don't dispatch again @@ -399,13 +453,13 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList if (savedInstanceState != null) return; - showDifferentSubFragments(internalScreen); + showDifferentSubFragments(initialScreen); } /** * Installs the initial child fragment based on the arguments supplied as arguments */ - private void showDifferentSubFragments(@NonNull InternalScreen screen) { + private void showDifferentSubFragments(@NonNull InitialScreen screen) { boolean firstTime = !initialScreenShown; switch (screen) { case NEARBY_STOPS: @@ -451,6 +505,39 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } + public void setSuggestionsStopRecyclerView(List stops, boolean showMessage){ + + var res = !stops.isEmpty(); + var query = searchStopsViewModel.getQueryStops(); + //Log.d(DEBUG_TAG, "received stops suggestions, are "+stops.size()); + if(res){ + if(suggestionsAdapter!=null){ + suggestionsAdapter.setStops(stops); + } else{ + suggestionsAdapter = new StopNearbyAdapter(new ArrayList<>(stops), null, true, (stop)->{ + requestArrivalsForStopID(stop.ID); + //clear query + emptySearchQueryViewModel(); + //setSuggestionsStopRecyclerView(List.of(), false); + }); + suggestionsRecyclerView.setAdapter(suggestionsAdapter); + } + suggestionsRecyclerView.setVisibility(View.VISIBLE); + + } else{ + if(suggestionsAdapter !=null) suggestionsAdapter = null; + suggestionsRecyclerView.setVisibility(View.GONE); + if(showMessage && query!=null && !query.trim().isEmpty()){ + stopSearchMessageTextView.setText( searchMode == SearchMode.SEARCH_NAME ? + R.string.no_stop_found_search_name : R.string.no_stop_found_search_number); + stopSearchMessageTextView.setVisibility(View.VISIBLE); + } else{ + stopSearchMessageTextView.setVisibility(View.GONE); + } + + } + } + public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment @@ -513,19 +600,20 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } if (setupOnStart) { if (pendingStopID==null){ - if(!pendingIntroRun){ //show the fragment //showButtonsFragment(); } - } else{ ///TODO: if there is a stop displayed, we need to hold the update } - setupOnStart = false; } + // this happens when the fragment is recreated and the query is set on the searchView + if(!searchStopsViewModel.getShowingSearchSuggestions()){ + emptySearchQueryViewModel(); + } } private void showButtonsFragment(boolean addInsteadOfReplace){ @@ -621,7 +709,6 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList requestArrivalsForStopID(pendingStopID); pendingStopID = null; } - //mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); //fragmentHelper.setBlockAllActivities(false); @@ -634,6 +721,7 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList //locationManager.removeLocationRequestFor(requester); //fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(); + searchStopsViewModel.saveOpenSearchSuggestions(); super.onPause(); } @@ -644,7 +732,8 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList @Override public void onQrScanSuccess(@NotNull String busIDToSearch) { - busStopSearchByIDEditText.setText(busIDToSearch); + EditText searchEditText = busStopSearchView.findViewById(androidx.appcompat.R.id.search_src_text); + searchEditText.setText(busIDToSearch); requestArrivalsForStopID(busIDToSearch); } @@ -664,13 +753,13 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList */ public void onSearchClick(View v) { //final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; - if (searchMode == SearchMode.SEARCH_ID) { - String busStopID = busStopSearchByIDEditText.getText().toString(); + if (searchMode == SearchMode.SEARCH_ID || searchMode == SearchMode.INITIAL) { + String busStopID = busStopSearchView.getQuery().toString(); fragmentHelper.stopLastRequestIfNeeded(); requestArrivalsForStopID(busStopID); } else if (searchMode == SearchMode.SEARCH_NAME) { // searchMode == SEARCH_BY_NAME - String query = busStopSearchByNameEditText.getText().toString(); + String query = busStopSearchView.getQuery().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { @@ -688,17 +777,17 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList public void onToggleKeyboardLayout(View v) { switch (searchMode){ case SEARCH_ID: - setSearchModeBusStopName(); - if (busStopSearchByNameEditText.requestFocus()) { + setSearchMode(SearchMode.SEARCH_NAME); + if (busStopSearchView.requestFocus()) showKeyboard(); - } + //} break; case SEARCH_NAME: case INITIAL: - setSearchModeBusStopID(); - if (busStopSearchByIDEditText.requestFocus()) { - showKeyboard(); - } + setSearchMode(SearchMode.SEARCH_ID); + if (busStopSearchView.requestFocus()) + showKeyboard(); + //} } } @@ -709,52 +798,52 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// + + private EditText getSearchEditText() { + return busStopSearchView.findViewById(androidx.appcompat.R.id.search_src_text); + } public void showKeyboard() { if(getActivity() == null) return; - InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - View view; - if(searchMode == SearchMode.SEARCH_ID) - view= busStopSearchByIDEditText; - else if(searchMode == SearchMode.SEARCH_NAME) - view = busStopSearchByNameEditText; - else{ - Log.e(DEBUG_TAG, "Asking to show keyboard but SearchMode is "+searchMode+", ignoring"); - return; - } - - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + final EditText et = getSearchEditText(); + et.requestFocus(); + et.post(() -> { + InputMethodManager imm = (InputMethodManager) + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) return; + imm.restartInput(et); // applica il nuovo inputType + imm.showSoftInput(et, InputMethodManager.SHOW_IMPLICIT); + }); } - private void setSearchModeBusStopID() { - searchMode = SearchMode.SEARCH_ID; - busStopSearchByNameEditText.setVisibility(View.GONE); - busStopSearchByNameEditText.setText(""); - busStopSearchByIDEditText.setVisibility(View.VISIBLE); - floatingActionButton.setImageResource(R.drawable.alphabetical); - } - private void setSearchModeBusStopName() { - searchMode = SearchMode.SEARCH_NAME; - busStopSearchByIDEditText.setVisibility(View.GONE); - busStopSearchByIDEditText.setText(""); - busStopSearchByNameEditText.setVisibility(View.VISIBLE); - floatingActionButton.setImageResource(R.drawable.numeric); - } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } - /** + private void setSearchMode(SearchMode mode) { + searchMode = mode; + busStopSearchView.setQuery("", false); + boolean byId = mode != SearchMode.SEARCH_NAME; + busStopSearchView.setInputType(byId ? InputType.TYPE_CLASS_NUMBER : InputType.TYPE_CLASS_TEXT); + floatingActionButton.setImageResource(byId ? R.drawable.numeric : R.drawable.alphabetical); + busStopSearchView.setQueryHint(getString(byId ? R.string.insert_bus_stop_number : R.string.insert_bus_stop_name)); + } + + /* * 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()); } + */ + @Nullable @Override public View getBaseViewForSnackBar() { @@ -777,12 +866,16 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList private void prepareGUIForArrivals() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); + //this is to hide the eventual search results from the text + //setSuggestionsStopRecyclerView(List.of(), false); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); + //setSuggestionsStopRecyclerView(List.of(), false); + //actionHelpMenuItem.setVisible(false); } @@ -832,6 +925,37 @@ public class MainScreenFragment extends BarcodeFragment implements FragmentList } + /* + // KEEPING THIS METHODS AS EXAMPLES FOR SHOW + boolean searchStopsResultsFragmentShown(){ + Fragment frag = childFragMan.findFragmentById(R.id.resultFrame); + return frag instanceof StopSearchResultFragment; + } + void showSearchStopsResultsFragment(){ + final String TAG = "stopSearchResult"; + Fragment frag = childFragMan.findFragmentById(R.id.resultFrame); + if (frag instanceof StopSearchResultFragment){ + return; //nothing to do + } + boolean exists = frag != null; + var trans = childFragMan.beginTransaction(); + + frag = childFragMan.findFragmentByTag(TAG); + if (!(frag instanceof StopSearchResultFragment)) { + frag = StopSearchResultFragment.newInstance(); + } + if(exists){ + //show the fragment + trans.replace(R.id.resultFrame,frag, TAG); + } else{ + //create new fragment + trans.add(R.id.resultFrame,frag, TAG); + } + trans.addToBackStack(null); + trans.commit(); + } + */ + @Override public void openLineFromStop(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt index 523fc45..2ea66e5 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.kt @@ -18,7 +18,6 @@ package it.reyboz.bustorino.fragments import android.content.Context -import android.content.res.ColorStateList import android.location.Location import android.os.Bundle import android.util.Log @@ -28,7 +27,6 @@ import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.AppCompatButton -import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView @@ -37,7 +35,7 @@ import com.google.android.material.button.MaterialButton import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.ArrivalsStopAdapter -import it.reyboz.bustorino.adapters.SquareStopAdapter +import it.reyboz.bustorino.adapters.StopNearbyAdapter import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.DatabaseUpdate import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager @@ -46,7 +44,6 @@ import it.reyboz.bustorino.middleware.FusedNativeLocationProvider.LocationUpdate import it.reyboz.bustorino.util.Permissions import it.reyboz.bustorino.util.Permissions.Companion.bothLocationPermissionsGranted import it.reyboz.bustorino.util.StopSorterByDistance -import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -81,7 +78,7 @@ class NearbyStopsFragment : ScreenBaseFragment() { private lateinit var gridRecyclerView: RecyclerView - private var dataAdapter: SquareStopAdapter? = null + private var dataAdapter: StopNearbyAdapter? = null private var gridLayoutManager: AutoFitGridLayoutManager? = null private var lastPosition: GPSPoint? = null private var circlingProgressBar: ProgressBar? = null @@ -732,7 +729,9 @@ class NearbyStopsFragment : ScreenBaseFragment() { Collections.sort(stops, StopSorterByDistance(location)) if (dataAdapter == null) { - dataAdapter = SquareStopAdapter(stops, mListener, lastPosition) + dataAdapter = StopNearbyAdapter(ArrayList(stops), lastPosition, false){ + stop-> mListener?.requestArrivalsForStopID(stop.ID) + } firstLocForStops = false } else { dataAdapter!!.setUserPosition(lastPosition) diff --git a/app/src/main/java/it/reyboz/bustorino/util/StopSorterSearch.kt b/app/src/main/java/it/reyboz/bustorino/util/StopSorterSearch.kt new file mode 100644 index 0000000..e7dda18 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/util/StopSorterSearch.kt @@ -0,0 +1,30 @@ +package it.reyboz.bustorino.util + +import it.reyboz.bustorino.backend.Stop + +class StopSorterSearch(val searchById: Boolean): Comparator { + override fun compare(s0: Stop?, s1: Stop?): Int { + if (s0 ==null) { + if (s1 != null) return 1 + else return 0 + } + else if(s1 == null){ + //s0 is not null + return -1 + } + if(s0.location==null){ + if(s1.location != null) return 1 + } else if(s1.location==null) return -1 + + if(searchById){ + val lenDiff = s0.ID.length - s1.ID.length + if(lenDiff!=0) return lenDiff + //second criterion + val diff = s0.ID.toInt() - s1.ID.toInt() + //if(diff!=0) return diff + return diff + } else{ + return -1*(s0.numRoutesStopping - s1.numRoutesStopping) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/SearchStopsDBViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/SearchStopsDBViewModel.kt new file mode 100644 index 0000000..ccf9cad --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/SearchStopsDBViewModel.kt @@ -0,0 +1,90 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.liveData +import androidx.lifecycle.map +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.NextGenDB +import it.reyboz.bustorino.util.StopSorterSearch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlin.math.min +import kotlin.time.Duration.Companion.milliseconds + +class SearchStopsDBViewModel(application: Application): AndroidViewModel(application) { + + private data class Request(val q: String, val byId: Boolean) + + private val nextGenDB = NextGenDB.getInstance(application) + + var showingSearchSuggestions = false + + + private val queryLiveData = MutableLiveData(Request("", true)) + private val numberLimitStop = MutableLiveData(20) + val queryResultsLiveData = queryLiveData.distinctUntilChanged().switchMap { r-> + liveData(viewModelScope.coroutineContext + Dispatchers.IO, timeoutInMs = 0) { + if (r.q.isEmpty()) { + //showQueryResult.postValue(false) + emit(emptyList()) + } + else { + delay(250.milliseconds) + //showQueryResult.postValue(true) + val list = nextGenDB.searchStopsByCodeOrName(r.q, r.byId) + /*if (r.byId) + emit(list.sortedBy { it.ID }) + else + emit(list.sortedBy { it.numRoutesStopping }.reversed()) + + */ + emit(list.sortedWith(StopSorterSearch(r.byId))) + } + } + } + + val showQueryResult = queryResultsLiveData.map { it.isNotEmpty() } + + fun searchStops(query: String, byId: Boolean){ + //Log.d(TAG, "setting query $query with id $byId, was previously ${queryLiveData.value}") + this.queryLiveData.value = Request(query, byId) + } + fun getQueryStops(): String?{ + return queryLiveData.value?.q + } + fun setLimitSearchStops(num: Int){ + numberLimitStop.value = num + } + + + val filteredStopsSearch = MediatorLiveData>() + private fun setFilteredStops(stops:List, num: Int){ + filteredStopsSearch.postValue(stops.subList(0, min(num, stops.size))) + } + + init { + filteredStopsSearch.addSource(queryResultsLiveData) { + setFilteredStops(it, numberLimitStop.value!!) + } + filteredStopsSearch.addSource(numberLimitStop) { + queryResultsLiveData.value?.let{ stops -> + setFilteredStops(stops, it) + } + } + } + + fun saveOpenSearchSuggestions(){ + showingSearchSuggestions = showQueryResult.value?: false + } + + companion object{ + private const val TAG = "BusTO-SearchStopsVM" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml index 4c96a87..e482100 100644 --- a/app/src/main/res/layout/fragment_main_screen.xml +++ b/app/src/main/res/layout/fragment_main_screen.xml @@ -6,115 +6,156 @@ tools:context=".fragments.MainScreenFragment" android:paddingTop="10dip" > - - - + + - + /> + - - + android:background="@drawable/search_button_custom" + android:contentDescription="@string/search" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/barrierSearch" + app:layout_constraintStart_toEndOf="@id/busStopSearchView" + app:layout_constraintEnd_toStartOf="@id/QRButton" + android:visibility="gone" + /> + + + + + + + + - + - + + - + + + + + --> + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 682c36d..6ce415e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -267,4 +267,6 @@ Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Nessuna informazione trasmessa sulla direzione o sul viaggio del veicolo. Download in corso delle informazioni sul viaggio + Nessuna fermata trovata con questo nome + Nessuna fermata trovata con questo numero diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d627d3..829603a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -164,6 +164,8 @@ Finding location No stops nearby + No stop found matching this name + No stop found matching this number Loading arrival times No stops nearby to search arrival times for Minimum number of stops