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+" = ?"; + } 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,12 +22,14 @@ import android.content.pm.PackageManager; import android.os.Bundle; +import android.text.InputType; 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; @@ -49,7 +51,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.Map; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import it.reyboz.bustorino.BuildConfig; @@ -57,6 +58,7 @@ 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 +83,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 +91,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 +115,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,7 +141,7 @@ private String pendingStopID = null; private String pendingSearchQuery = null; - private InternalScreen internalScreen = InternalScreen.HOME_BUTTONS; + private InitialScreen initialScreen = InitialScreen.HOME_BUTTONS; private CoordinatorLayout coordLayout; //this is really a hackish thing, but it works @@ -203,14 +206,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 +228,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 +236,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 +257,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 +269,6 @@ } fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); - } @Override @@ -285,14 +287,17 @@ // 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 + EditText searchEditText = busStopSearchView.findViewById(androidx.appcompat.R.id.search_src_text); + searchEditText.setSelectAllOnFocus(true); + //busStopSearchView.setSelectAllOnFocus(true); + /*busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { @@ -301,7 +306,9 @@ } return false; }); - busStopSearchByNameEditText + + */ + searchEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { @@ -311,6 +318,19 @@ return false; }); + busStopSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String q) { + return false; + } + + @Override + public boolean onQueryTextChange(String q) { + searchStopsViewModel.searchStops(q, searchMode != SearchMode.SEARCH_NAME); + return true; + } + }); + swipeRefreshLayout .setOnRefreshListener(this::refreshStop); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); @@ -319,7 +339,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 +351,8 @@ setSearchModeBusStopName(); }); + */ + AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); @@ -381,16 +403,18 @@ 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; + searchStopsViewModel = new ViewModelProvider(this).get(SearchStopsDBViewModel.class); + + searchStopsViewModel.getQueryResultsLiveData().observe(getViewLifecycleOwner(), stops ->{ + var res = !stops.isEmpty(); + Log.d(DEBUG_TAG, "received stops suggestions, are "+stops.size()); + if(res){ + if(!searchStopsResultsFragmentShown()) + showSearchStopsResultsFragment(); + } else if (searchStopsResultsFragmentShown()) { + childFragMan.popBackStack(); //remove the fragment } - } - - */ + }); if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); // The child FragmentManager has restored its content — don't dispatch again @@ -399,13 +423,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: @@ -644,7 +668,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); } @@ -665,12 +690,12 @@ public void onSearchClick(View v) { //final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SearchMode.SEARCH_ID) { - String busStopID = busStopSearchByIDEditText.getText().toString(); + 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,15 +713,15 @@ 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()) { + setSearchMode(SearchMode.SEARCH_ID); + if (busStopSearchView.requestFocus()) { showKeyboard(); } } @@ -712,7 +737,7 @@ public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - View view; + /*View view; if(searchMode == SearchMode.SEARCH_ID) view= busStopSearchByIDEditText; else if(searchMode == SearchMode.SEARCH_NAME) @@ -721,10 +746,12 @@ Log.e(DEBUG_TAG, "Asking to show keyboard but SearchMode is "+searchMode+", ignoring"); return; } + */ - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(busStopSearchView, InputMethodManager.SHOW_IMPLICIT); } + /* private void setSearchModeBusStopID() { searchMode = SearchMode.SEARCH_ID; busStopSearchByNameEditText.setVisibility(View.GONE); @@ -740,21 +767,41 @@ 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) { + 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)); + searchMode =mode; + } + + /* * 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() { @@ -832,6 +879,34 @@ } + 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/StopSearchResultFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/StopSearchResultFragment.kt new file mode 100644 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/StopSearchResultFragment.kt @@ -0,0 +1,57 @@ +package it.reyboz.bustorino.fragments + +import android.os.Bundle +import android.view.View +import android.widget.ListView +import androidx.lifecycle.ViewModelProvider +import it.reyboz.bustorino.adapters.StopAdapter +import it.reyboz.bustorino.viewmodels.SearchStopsDBViewModel +import kotlin.math.min + +class StopSearchResultFragment: ResultListFragment() { + + + private var stopAdapter: StopAdapter? = null + + private var searchViewModel: SearchStopsDBViewModel? = null + + private lateinit var listView: ListView + + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + + + searchViewModel = ViewModelProvider(requireActivity()).get(SearchStopsDBViewModel::class.java) + searchViewModel?.let{ + it.queryResultsLiveData.observe(viewLifecycleOwner){ stops -> + + val stopsShow = stops.sortedBy { s-> s.numRoutesStopping }.reversed() + stopAdapter?.let{ + + } + val adapter = StopAdapter(requireContext(), stopsShow.subList(0, min(10, stopsShow.size)),) + //resetListAdapter(adapter) + resultsListView.adapter = adapter + resultsListView.visibility = View.VISIBLE + } + } + } + + companion object { + @JvmStatic + fun newInstance(): StopSearchResultFragment { + val args = Bundle() + //TODO: search stops inside the DB + //args.putString(StopListFragment.WORDS_SEARCHED, searchQuery) + val fragment = StopSearchResultFragment() + args.putSerializable(LIST_TYPE, FragmentKind.STOPS) + fragment.setArguments(args) + return fragment + } + } + + +} \ 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,53 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +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) + + + private val queryLiveData = MutableLiveData(Request("", true)) + 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) + emit(nextGenDB.searchStopsByCodeOrName(r.q, r.byId)) + } + } + } + + val showQueryResult = queryResultsLiveData.map { it.isNotEmpty() } + + val runningJob: Job? = null + + fun searchStops(query: String, byId: Boolean){ + Log.d(TAG, "setting query $query with id $byId") + this.queryLiveData.value = Request(query, byId) + } + + 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 @@ -26,43 +26,19 @@ app:layout_constraintStart_toEndOf="@id/QRButton" /> - - - - - - + app:layout_constraintEnd_toStartOf="@id/searchButton" />