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 --- /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 rename from app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java rename to app/src/main/java/it/reyboz/bustorino/adapters/StopNearbyAdapter.java --- 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.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 SquareViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); //sort the stops by distance - if(stops != null && stops.size() > 0) - Collections.sort(stops,new StopSorterByDistance(userPosition)); + 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 @@ if(whatStopsHere == null) { holder.routesView.setVisibility(View.GONE); } else { - holder.routesView.setText(context.getString(R.string.lines_fill, whatStopsHere)); + holder.routesView.setText(whatStopsHere); + //context.getString(R.string.lines_fill, whatStopsHere)); holder.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } - holder.stopID =stop.ID; + holder.itemView.setOnClickListener(view -> {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 @@ return stops.size(); } - class SquareViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + class SquareViewHolder extends RecyclerView.ViewHolder { TextView stopIDView; TextView stopNameView; + TextView locationTextView; TextView routesView; TextView distancetextView; - String stopID; + //Stop stop; SquareViewHolder(View holdView){ super(holdView); - holdView.setOnClickListener(this); + //holdView.setOnClickListener(this); stopIDView = (TextView) holdView.findViewById(R.id.stop_numberText); stopNameView = (TextView) holdView.findViewById(R.id.stop_nameText); routesView = (TextView) holdView.findViewById(R.id.stop_linesText); + locationTextView = holdView.findViewById(R.id.stop_locationTextView); distancetextView = (TextView) holdView.findViewById(R.id.stop_distanceTextView); } - @Override + /*@Override public void onClick(View v) { - listener.requestArrivalsForStopID(stopID); + listener.onStopClick(stop); } + */ + } public void setStops(List 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 --- 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 @@ }); } + @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 --- 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.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 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 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 @@ 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 @@ 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 @@ 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 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 @@ * @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 @@ 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 @@ 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 @@ } fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); - } @Override @@ -285,14 +304,31 @@ // 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 @@ } return false; }); - busStopSearchByNameEditText + + */ + /*searchEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { @@ -311,6 +349,24 @@ 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 @@ 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 @@ setSearchModeBusStopName(); }); + */ + AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); @@ -379,18 +437,14 @@ @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 @@ 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 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 @@ } 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 @@ requestArrivalsForStopID(pendingStopID); pendingStopID = null; } - //mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); //fragmentHelper.setBlockAllActivities(false); @@ -634,6 +721,7 @@ //locationManager.removeLocationRequestFor(requester); //fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(); + searchStopsViewModel.saveOpenSearchSuggestions(); super.onPause(); } @@ -644,7 +732,8 @@ @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 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 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 @@ } ////////////////////////////////////// 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 @@ 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 @@ } + /* + // 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 --- 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.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 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.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 @@ 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 @@ 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 --- /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 --- /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 --- 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" + /> + + + + + + + + - + - + + - + + + + + --> + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -267,4 +267,6 @@ 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 --- 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