1

I'm trying to implement SearchView in a Fragment, with search results queried for directly from sqlite via CursorLoader and search results rendered in the same Fragment via custom CursorAdapter. Search suggestions are also queried for directly from sqlite and rendered via custom CursorAdapter (CountrySuggestionsAdaptor). So, I have one CursorLoader for getting suggestions(loader id =0) and the other CursorLoader for getting search results(loader id=1). There are 3 problems with loading suggestions at the moment:

1) Typing the first letter doesn't show any suggestions, i.e. it doesn't call bindView of the custom CursorAdapter. It only starts showing suggestions after second typing

2)If I type "Un" it will give suggestions, then if I type "Ur" it will give this error

android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method. at android.database.AbstractWindowedCursor.checkPosition(AbstractWindowedCursor.java:139) at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:74) at android.support.v4.widget.CursorAdapter.getItemId(CursorAdapter.java:226) at android.widget.AutoCompleteTextView.buildImeCompletions(AutoCompleteTextView.java:1132) at android.widget.AutoCompleteTextView.showDropDown(AutoCompleteTextView.java:1091) at android.widget.AutoCompleteTextView.updateDropDownForFilter(AutoCompleteTextView.java:974) at android.widget.AutoCompleteTextView.onFilterComplete(AutoCompleteTextView.java:956) at android.widget.Filter$ResultsHandler.handleMessage(Filter.java:285) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:135) at android.app.ActivityThread.main(ActivityThread.java:5294) at java.lang.reflect.Method.invoke(Native Method) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:904) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:699)

3) First I type "Un" it gives suggestions, I click on the first suggestion and it successfully renders me a list based on it. Then I additionally type the third letter into search view, such as "Uni" and it crashes giving me this letter:

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteQuery: Select _id,countryNameRu, countryId FROM countries WHERE countryNameRu LIKE 'Un%' at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55) at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:58) at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:152) at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:124) at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:214) at android.support.v4.widget.CursorAdapter.getItemId(CursorAdapter.java:225) at android.widget.AdapterView.rememberSyncState(AdapterView.java:1226) at android.widget.AdapterView$AdapterDataSetObserver.onChanged(AdapterView.java:820) at android.widget.AbsListView$AdapterDataSetObserver.onChanged(AbsListView.java:6156) at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37) at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50) at android.support.v4.widget.CursorAdapter.swapCursor(CursorAdapter.java:347) at android.support.v4.widget.CursorAdapter.changeCursor(CursorAdapter.java:315) at android.support.v4.widget.CursorFilter.publishResults(CursorFilter.java:68) at android.widget.Filter$ResultsHandler.handleMessage(Filter.java:282) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:135) at android.app.ActivityThread.main(ActivityThread.java:5294) at java.lang.reflect.Method.invoke(Native Method) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:904) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:699)

I've been looking for solutions all over the internet for several days now, I'm kinda desperate at the moment. I thought about implementing ContentProvider, but how would it help and is it absolutely necessary?

The fragment:

import android.database.Cursor;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.widget.SearchView;


import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Callback;

public class RoamingTariffs extends ProgressListFragment implements  LoaderManager.LoaderCallbacks<Cursor>{
    private RoamingTariffSearchResultsAdapter roamingTariffAdapter;
    private CountrySuggestionsAdaptor mSearchViewAdapter;
    RoamingTariffDbHelper dbHelper;

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.roaming_toolbar, menu);
        mSearchViewAdapter = new CountriesSearchResultsAdaptor(this.getActivity(),null);
        SearchView searchView  = (SearchView) menu.findItem(R.id.menu_search).getActionView();

        searchView.setSuggestionsAdapter(mSearchViewAdapter);
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener(){

            @Override
            public boolean onQueryTextSubmit(String s) {
                if(!s.isEmpty())
                  loadSuggestions(s,false);
                return true;
            }

            @Override
            public boolean onQueryTextChange(String s) {
                if(!s.isEmpty())
                    loadSuggestions(s,false);
                return true;
            }
        });

        searchView.setOnSuggestionListener(
                new SearchView.OnSuggestionListener(){

                    @Override
                    public boolean onSuggestionSelect(int position) {
                        Cursor cursor = (Cursor) mSearchViewAdapter.getItem(position);
                        String countryId1 = cursor.getString(1);
                        loadCountryDetails(countryId1);
                        return true;
                    }

                    @Override
                    public boolean onSuggestionClick(int position) {
                        Cursor cursor = (Cursor) mSearchViewAdapter.getItem(position);
                        String countryId1 = cursor.getString(1);
                        loadCountryDetails(countryId1);
                        return false;
                    }
                }
        );
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setHasOptionsMenu(true);

        roamingTariffAdapter = new RoamingTariffCursorAdapter(getActivity(), null);
        getListView().setAdapter(roamingTariffAdapter);

        dbHelper = new RoamingTariffDbHelper(getActivity());

        ((LoginActivity) getActivity()).getSupportActionBar().setTitle("Roaming Tariffs");
   RoamingTariffInterface roamingTariffService =
                RoamingTariffClient.getClient().create(RoamingTariffInterface.class);
        Call<List<CountryDto>> call = roamingTariffService.getCountries(countryId);
        call.enqueue(new Callback<List<CountryDto>>() {
            @Override
            public void onResponse(Call<List<CountryDto>> call, Response<List<CountryDto>> response) {
                List<CountryDto> countryList = response.body();
                if (countryList == null)
                    countryList = new ArrayList<CountryDto>();

                // CREATE SQLITE TABLE AND SAVE
                RoamingTariffDbHelper helper = new RoamingTariffDbHelper(getActivity());

                for (CountryDto countryDto : countryList) {
                    helper.addCountryEntry(countryDto);
                }

            }

            @Override
            public void onFailure(Call<List<CountryDto>> call, Throwable t) {
                showError();
            }
        });
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        showProgress();
        CursorLoader cursorLoader = null;
        switch (id){
            case 0://loadSuggestions
                final String queryText = args.getString("queryText");
                cursorLoader =  new CursorLoader(getActivity()) {
                    @Override
                    public Cursor loadInBackground() {
                        Cursor cursor =  dbHelper.getCountryNamesBySearchLetters(queryText);
                        return cursor;
                    }
                };
                break;
            case 1://load results
                final String countryId = args.getString("countryId");
                cursorLoader =  new CursorLoader(getActivity()) {
                    @Override
                    public Cursor loadInBackground() {
                        Cursor cursor =  dbHelper.getOperatorsByCountryId(countryId);
                        return cursor;
                    }
                };
                break;
        }
        return cursorLoader;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        int loaderId = loader.getId();
        switch(loaderId){
            case 0://suggestions are ready to show
                if (data != null)
                    mSearchViewAdapter.swapCursor(data);
                break;
            case 1://search results are ready to show
                if(data !=null){  
                roamingTariffAdapter.swapCursor(data);}
                break;
        }
        showContent();
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mSearchViewAdapter.swapCursor(null);
        roamingTariffAdapter.swapCursor(null);
    }

    public void loadSuggestions(String query){
        Bundle bundle = new Bundle();
        bundle.putString("queryText", query);
        getLoaderManager().restartLoader(0, bundle, this);
    }

    public void loadCountryDetails(String countryId){
        Bundle bundle = new Bundle();
        bundle.putString("countryId", countryId);
        getLoaderManager().restartLoader(1, bundle, this);
    }
...
}

The suggestion adapter:

public class CountrySuggestionsAdaptor extends CursorAdapter {
    private Context context = null;

    public CountrySuggestionsAdaptor(Context context, Cursor c) {
        super(context, c, 0);
    }


    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context).inflate(R.layout.country_suggestion_item,parent, false);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView countryView = (TextView)view.findViewById(R.id.country_item);
        countryView.setText(cursor.getString(1));
    }
}

The relevant query method from RoamingTariffDbHelper:

public Cursor getCountryNamesBySearchLetters(String startingStr){
        SQLiteDatabase db = this.getReadableDatabase();
        String query = RoamingTariffDbContract.SQL_SELECT_COUNTRIES_START_WITH+
                Helper.firstLetterToCapital(startingStr)+"%'";
        Cursor cursor = db.rawQuery(query,null);
        if(cursor == null){
            return null;
        }
        else if(!cursor.moveToFirst()) {
            cursor.close();
            return null;
        }
        return cursor;
    }

Edit: I followed @pskink's advice and removed querytextlisteners and added this instead

 FilterQueryProvider fqp =new FilterQueryProvider() {
            @Override
            public Cursor runQuery(CharSequence constraint) {
                Cursor cursor = null;
                if(constraint.length()!=0)
                   cursor = dbHelper.getCountryNamesBySearchLetters(constraint.toString());
              return cursor;
            }
        };

Problems 2 and 3 are gone. Problem number 1 persists. I've debugged inside runQuery, on the first type constraint is null for some reason, hence no suggestions. Although constraint should be the first letter I've typed. What's the reason?

Nazerke
  • 2,098
  • 7
  • 37
  • 57
  • problem is cursor already closed http://stackoverflow.com/questions/3232002/can-someone-explain-to-me-this-staledataexception. Checking your code to identify exact point – Ramit Aug 25 '16 at 05:42
  • I've added another problem, can you look at it please – Nazerke Aug 25 '16 at 06:01
  • please debug result of Helper.firstLetterToCapital(startingStr) or paste code – Ramit Aug 25 '16 at 06:04
  • yes, instead of custom `CursorLoader` you should use custom `ContentProvider` and setup `FilterQueryProvider` on your `CursorAdapter` – pskink Aug 25 '16 at 06:45
  • @Ramit it's nothing. just a capitalizer public static String firstLetterToCapital(String locale){ return Character.toUpperCase(locale.charAt(0)) + locale.substring(1); } – Nazerke Aug 25 '16 at 06:50
  • @pskink can you please explain why is it necessary to write ContentProvider if I'm not going to share the data with other applications? – Nazerke Aug 25 '16 at 06:51
  • it is the preferred way when working with `Loader`s, but it is not necessary, just setup `FilterQueryProvider` and remove `setOnQueryTextListener()` method, calling `setSuggestionsAdapter` is enough for it to work – pskink Aug 25 '16 at 06:52
  • @pskink and why the cursor isn't being closed in this code?isn't it supposed to happen in swapCursor? – Nazerke Aug 25 '16 at 06:55
  • public Cursor swapCursor (Cursor newCursor): `"""Swap in a new Cursor, returning the old Cursor. Unlike changeCursor(Cursor), the returned old Cursor is not closed."""` – pskink Aug 25 '16 at 07:00
  • sorry I meant why the cursor is already closed?(from error message of the 2nd problem). also why the problem number 1 is happening? – Nazerke Aug 25 '16 at 07:15
  • why dont you simply use `FQP`? just setup FilterQueryProvider and remove setOnQueryTextListener() method, calling setSuggestionsAdapter is enough for it to work – pskink Aug 25 '16 at 07:16
  • @pskink I will, I was just curious. Can you pls give me link to a working example? Can't seem to find any in the developer guide – Nazerke Aug 25 '16 at 08:43
  • just call CursorAdapter.setFilterQueryProvider – pskink Aug 25 '16 at 09:08
  • @pskink thank you, I seem to be progressing. I've edited the question, can you have a look at the remaining problem please – Nazerke Aug 25 '16 at 09:32
  • rrad about the threshold – pskink Aug 25 '16 at 09:43
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/121827/discussion-between-nazerke-and-pskink). – Nazerke Aug 25 '16 at 10:48
  • google for `searchview threshold` – pskink Aug 25 '16 at 15:34
  • @pskink yes I figured. it is all solved now. thank you. I'll write up the answer later – Nazerke Aug 26 '16 at 03:40

0 Answers0