So I am having trouble extending the MultiAutoCompleteTextView
and backing it with a CursorLoader
, while simultaneously using a custom Tokenizer
. The issue rises specifically with the mAdapter.setCursorToStringConverter();
call. The convertToString()
method which has a Cursor as an argument has a valid and unclosed cursor upon the first call to this method. However subsequent calls result in either a null cursor or a closed cursor. I am guessing this has something to do with how the LoaderManager
manages the CursorLoader
.
If I comment the setCursorToStringConverter()
method out, then I do see a list of available choices based on the text I entered into this view. However, since there is no convertToString()
method implemented, then the terminateToken()
method of the custom Tokenizer
does not receive the string that I intend it to be, but rather a representative string of the cursor object, since the cursor has not been used to get the current string value of a desired column in the resulting query.
Has anyone been able to implement the combination of the three classes (CursorLoader/LoaderManger
, MultiAutoCompleteTextView
, and Tokenizer
) ?
Am I going in the right direction with this, or is this simply not possible?
I have been able to implement a custom MultiAutoCompleteTextView
backed by a SimpleCursorAdapter
along with a custom Tokenizer
. I was just wondering if its possible to implement this using a CursorLoader
instead, since Strict Mode complains about the cursor in MultiAutoCompleteTextView
not being explicitly closed.
Any help would be greatly appreciated.
public class CustomMultiAutoCompleteTextView extends MultiAutoCompleteTextView
implements LoaderManager.LoaderCallbacks<Cursor> {
private final String DEBUG_TAG = getClass().getSimpleName().toString();
private Messenger2 mContext;
private RecipientsCursorAdapter mAdapter;
private ContentResolver mContentResolver;
private final char delimiter = ' ';
private CustomMultiAutoCompleteTextView mView;
// If non-null, this is the current filter the user has provided.
private String mCurFilter;
// These are the Contacts rows that we will retrieve.
final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME };
public CustomMultiAutoCompleteTextView(Context c) {
super(c);
init(c);
}
public CustomMultiAutoCompleteTextView(Context c, AttributeSet attrs) {
super(c, attrs);
init(c);
}
private void init(Context context) {
mContext = (Messenger2) context;
mContentResolver = mContext.getContentResolver();
mView = this;
mAdapter = new RecipientsCursorAdapter(mContext, 0, null, new String[0], new int[0], mContext);
mAdapter.setCursorToStringConverter(new CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor c) {
String contactName = c.getString(c.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME));
return contactName;
}
});
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.d(DEBUG_TAG, "onTextChanged()");
if (!s.equals(""))
mCurFilter = s.toString();
else
mCurFilter = "";
mContext.getLoaderManager().restartLoader(0, null, mView);
}
@Override
public void afterTextChanged(Editable s) {
}
});
setAdapter(mAdapter);
setTokenizer(new SpaceTokenizer());
mContext.getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Log.d(DEBUG_TAG, "onCreateLoader()");
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_FILTER_URI,Uri.encode(mCurFilter));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME
+ " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";
return new CursorLoader(mContext, baseUri, CONTACTS_SUMMARY_PROJECTION,
selection, null, sortOrder);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing
// the old cursor once we return.)
Log.d(DEBUG_TAG, "onLoadFinished()");
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
Log.d(DEBUG_TAG, "onLoaderReset()");
mAdapter.swapCursor(null);
}
private class SpaceTokenizer implements Tokenizer {
public int findTokenStart(CharSequence text, int cursor) {
int i = cursor;
while (i > 0 && text.charAt(i - 1) != delimiter) {
i--;
}
while (i < cursor && text.charAt(i) == delimiter) {
i++;
}
return i;
}
public int findTokenEnd(CharSequence text, int cursor) {
int i = cursor;
int len = text.length();
while (i < len) {
if (text.charAt(i) == delimiter) {
return i;
} else {
i++;
}
}
return len;
}
public CharSequence terminateToken(CharSequence text) {
Log.d(DEBUG_TAG, "terminateToken()");
int i = text.length();
while (i > 0 && text.charAt(i - 1) == delimiter) {
i--;
}
if (i > 0 && text.charAt(i - 1) == delimiter) {
return text;
} else {
CharSequence contactName = createContactBubble(text);
return contactName;
}
}
}
}
UPDATE 1
I am now calling the setStringConversionColumn()
method instead of the setCursorToStringConverter()
as @Olaf suggested. I have set this in the onLoadFinished()
since this is the only time the Cursor
is available as this is implementing a LoaderManger
.
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing
// the old cursor once we return.)
Log.d(DEBUG_TAG, "onLoadFinished()");
mAdapter.setStringConversionColumn(data.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME));
mAdapter.swapCursor(data);
}
This works in selecting one item for the MultiAutoCompleteTextView
, but will not allow multiple items to be selected in the MultiAutoCompleteTextView
.
I am guessing there is some issue with the onTextChanged()
method since it calls restartLoader()
. This works for the first entry in this view but not for subsequent entries. I'm not too sure at this point what is wrong.
UPDATE 2
So I have identified the issue. The problem is the TextWatcher's onTextChanged()
method. After making the selection to terminate the first token ( let's say the token was "Joe Johnson" ), then entering more characters into this MultiAutoCompleteTextView
( such as al
) the value of the arg s
that gets passed into the onTextChanged()
method now contains not only the additionally added characters but also the characters from the token which has previously been terminated ( the value of s
at this point is Joe Johnson al
). Now the value of mCursor
gets set to Joe Johnson al
which subsequently gets passed into the query in onCreateLoader()
which will obviously return no results. Are there any ways around this situation? I am open to any suggestions.
UPDATE 3
When I implemented a custom MultiAutoCompleteTextView
backed by a SimpleCursorAdapter
along with a custom Tokenizer
I set a FilterQueryProvider
like this:
mAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence constraint) {
Log.d(DEBUG_TAG, "runQuery() : constraint " + constraint);
Uri baseUri;
if (constraint != null) {
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(constraint.toString()));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME
+ " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";
Cursor c = mContentResolver.query(baseUri,
CONTACTS_SUMMARY_PROJECTION, selection, null, sortOrder);
return c;
}
});
And for some reason the runQuery()
method gets called twice from the TextWatcher's onTextChanged()
method:
public void onTextChanged(CharSequence s, int start, int before,
int count) {
Log.d(DEBUG_TAG, "onTextChanged() : s " + s);
mAdapter.getFilterQueryProvider().runQuery(s);
}
So in my previous example, the constraint
variable that gets passed into the runQuery()
method the first time is Joe Johnson al
. Then the second time runQuery()
method is called the value of the constraint
variable is al
. I don't know why the runQuery()
method runs twice when its only called once in the onTextChanged()
method.
Basically, androids autocomplete textview is not very powerful, when I have to deal with bigger amounts of data, what I do is, i keep a text change listener to the edit text for search, and then whenever something is changed on the edit text, it queries database.
If this might help someone, placing an edittext on onCreate
EditText etSearch = (EditText)findViewById(R.id.etSearchBox);
etSearch.addTextChangedListener(filterTextWatcher);
//The filterTextWatcher is
private TextWatcher filterTextWatcher = new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,int count) {
adapter.getFilter().filter(s.toString());
}
};
So, in your adapter, you need to create a getFilter() method...
@Override
public Filter getFilter() {
if (nameFilter == null) {
nameFilter = new NameFilter();
}
return nameFilter;
}
private class NameFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
Cursor cursor = null;
// get your cursor by passing appropriate query here
results.values = cursor;
results.count = cursor.getCount();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyDataSetChanged();
}
}