Android: How to get search suggestions asynchronously from the web? - android

I have created a searchable activity. Now, i want to add search suggestions that are taken from web service. I want to get those suggestions asynchronously. According to Adding Custom Suggestions I need to override the query method, do my suggestion search, build my own MatrixCursor and return it. but this is the problem, my request for getting the suggestion is an asynchronically one. so when result is back from net it out side of query method's scope.

Here is an example of SearchView with suggestions coming from a network service (I used Retrofit):
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_search_activity, menu);
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search));
final CursorAdapter suggestionAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
null,
new String[]{SearchManager.SUGGEST_COLUMN_TEXT_1},
new int[]{android.R.id.text1},
0);
final List<String> suggestions = new ArrayList<>();
searchView.setSuggestionsAdapter(suggestionAdapter);
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
#Override
public boolean onSuggestionSelect(int position) {
return false;
}
#Override
public boolean onSuggestionClick(int position) {
searchView.setQuery(suggestions.get(position), false);
searchView.clearFocus();
doSearch(suggestions.get(position));
return true;
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
MyApp.autocompleteService.search(newText, new Callback<Autocomplete>() {
#Override
public void success(Autocomplete autocomplete, Response response) {
suggestions.clear();
suggestions.addAll(autocomplete.suggestions);
String[] columns = {
BaseColumns._ID,
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA
};
MatrixCursor cursor = new MatrixCursor(columns);
for (int i = 0; i < autocomplete.suggestions.size(); i++) {
String[] tmp = {Integer.toString(i), autocomplete.suggestions.get(i), autocomplete.suggestions.get(i)};
cursor.addRow(tmp);
}
suggestionAdapter.swapCursor(cursor);
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(SearchFoodActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
Log.w("autocompleteService", error.getMessage());
}
});
return false;
}
});
return true;
}

It seems that the request to the suggestion content provider is not run on the UI thread, anyway, according to this answer: https://stackoverflow.com/a/12895381/621690 .
If you can change your http request you could simply call it blocking inside the query method. Might help to listen for interruptions or other signals (custom ones maybe) to stop unnecessary requests.
Another option - if you do not want to change any request classes that are already asynchronous (like if you are using Robospice) - should be to just return the MatrixCursor reference and populate it later on. The AbstractCursor class already implements the Observer pattern and sends out notifications in case of changes. If the search system is listening it should handle any changes in the data. I have yet to implement that myself so I cannot confirm that it will work out as nicely as I picture it. (Have a look at CursorLoader's source for more inspiration.)
And, anyway, isn't that the whole point of a cursor? Otherwise we could simply return a list with data.
UPDATE:
For me, using a MatrixCursor didn't work out. Instead, I have implemented two other solutions:
Using the AutoCompleteTextField in combination with a custom subclass of ArrayAdapter which itself uses a custom subclass of Filter. The method Filter#performFiltering() (which I override with the synchronous call to the remote service) is called asynchronously and the UI thread is not blocked.
Using the SearchWidget with a SearchableActivity and a custom ArrayAdapter (without custom Filter). When the search intent comes in, the remote request is started (Robospice) and when it comes back via callback, I call the following custom method on my ArrayAdapter<Tag> subclass:
public void addTags(List<Tag> items) {
if (items != null && items.size() > 0) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
super.setNotifyOnChange(false);
for (Tag tag : items) {
super.add(tag);
}
super.notifyDataSetChanged();
} else {
super.addAll(items);
}
}
}
This method takes care of triggering the notifications on the adapter and thus the search result list.

The closest thing I have found to solve this, is by using a ContentProvider and do the network request on the Query method of your provider (even when this goes against the best practices), with the result you can create a MatrixCursor as it's show here and here.
I'm still searching for other options like using a SyncAdapter, which seems overwhelming for the purpose of just showing suggestions that aren't used anywhere else.
Another option, that I took to do this asynchronously is to use an AutoCompleteTextView, that way you can create a custom adapter, where you can implement the getFilter function as it is shown in this answer.

Related

Using Observable.from and flatMap to perform operation on each item in an List and to set the result in each list element

I get a List of searchItems. For each element in this list exists a Lat und Lng. For these coordinates I use a googleService that takes these two values and returns a JsonObject with the name (city name) for this location. This works fine! In my onNext I can see in my log output the city.
Now my problem: I want to store this name in the corresponding list element. like: item.setLocation(loc) -> but I can not set access to the item in onNext() ! how can I get access to item ??
Observable.from(searchItems)
.flatMap(item -> googleService.getCityNameFromLatLngObserable(item.getLat(), item.getLng(), null)
.subscribeOn(Schedulers.io()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<JsonObject>() {
#Override
public void onCompleted() {
view.updateSearches(searchItem);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(JsonObject response) {
if (response.get("results").getAsJsonArray().size() > 0) {
String loc = response.get("results").getAsJsonArray()
.get(0).getAsJsonObject().get("address_components").getAsJsonArray()
.get(2).getAsJsonObject().get("long_name").toString();
Log.d("CONAN", "City: "+loc);
item.setLocation(loc); //does not work
}
}
});
Actually you shouldn't really change your searchItems but rather create new ones with the new info. This is to enforce immutability, which is very dear to Rx.
There is more than one way to do this, but here's one possible way. You need the item to call your Google service so you cannot use zip or any of its friends. So let's keep the first bit like you have it and get an observable emitting each search item:
Observable.from(searchItems)
Now let's go on and create new search items with their location.
Observable.from(searchItems)
.flatMap(item -> googleService
.getCityNameFromLatLngObserable(
item.getLat(),
item.getLng(), null)
.map(jsonObject -> /* create a
new item with location */))
.subscribeOn(Schedulers.io()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<JsonObject>() {
#Override
public void onCompleted() {
view.updateSearches(searchItem);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Item item) {
// Your items with location
}
});
What I did was map the results of the googleService. getCityNameFromLatLngObserable to an item that has the location from the result. Notice how here you have access to both the item and the json object return by google?
Now inside the map function you can either use a function such as createWithLocation(item, getLocation(jsonObject)) or use something like the builder pattern:
item.toBuilder()
.location(getLocation(jsonObject))
.build();
Notice also that on the subscriber onNext you have now an Item instead of a JsonObject and with the location filled. I assume you can code getLocation since your example already has this.
One last thing, you can also call the setter in the item instance and not create a new one. I would advise against this because you'll be adding a side effect to a call that others might not expect. It's usually a source for bugs. However, you can still do it :)
Hope it helps

Android how to search in empty query

I have an search view, below the search view contain edittext. i can check based on search view query and edittext content. Event the query is empty i need to search. Is this is
I have tried this but doesn't work
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
Log.i("MyApp", searchView.getQuery().toString());
consultarListaParadas(searchView.getQuery().toString());
getActivity().runOnUiThread(new Runnable() {
public void run() {
adaptadorListaParadasBus.setListaParadas(listaParadas);
adaptadorListaParadasBus.notifyDataSetChanged();
}
});
return true;
}
#Override
public boolean onQueryTextChange(String newText) {
if(newText.equals("")){
this.onQueryTextSubmit("");
}
return true;
}
});
Try with below condition inside onQueryTextChange() method, like below
if(TextUtils.isEmpty(newText)){
// Do your stuff here.
}
First thing first..
onQueryTextChange() will not be fired until you are not entering any text
, Instead what you can do is, make a call to search api with empty query param and get results...
Once text changes - You can use conditions given in other answers!! even your condition will work too.

Is there an advantage to using an Intent over an event listener launch in a search request?

I've looked over SO and cannot find a simple answer. Is there an advantage to using an Intent over an event listener on the search box for sending the text to the query event?
SearchView searchView =
(SearchView) MenuItemCompat.getActionView(searchMenuItem);
if(searchView != null){
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener(){
#Override
public boolean onQueryTextSubmit(String s) {
Toast.makeText(getApplicationContext(),
"String entered is " + s, Toast.LENGTH_SHORT).show();
return true;
}
#Override
public boolean onQueryTextChange(String s) {
return false;
}
});
}
Versus using:
private void handleIntent(Intent intent){
if(Intent.ACTION_SEARCH.equals(intent.getAction())){
String query = intent.getStringExtra(SearchManager.QUERY);
Log.d(LOG_TAG, "QUERY: " + query);
new FetchArtistTask().execute(query);
}
}
Advantages of Intent:
The Intent can be directed to other Activities besides one with SearchView;
You can have single Activity handle search requests from multiple other Activities;
You can make full use of different Activity start modes and task stack while handling the Intent;
In addition to using SearchView the search Intent can triggered outside of Activities (e.g. in Dialogs and PopupWindows) by using The Search Dialog, making searching experience in your application more unified;
You can make a search Intent yourself to send from Service, when user clicks Notification/appwidget etc.
Advantages of listener:
Can be used to filter suggestion list as user types.
These two approaches aren't mutually exclusive, so you may just use both.

Execute Searches While Typing

I have a working searchable Activity that queries a remote database upon submitting input in the ActionBar's android.support.v7.widget.SearchView (entering "Go" on the soft keyboard). This works fine, but I would ultimately like to query the database each time the SearchView's text changes via adding or removing a character. My initialization code of the SearchView is below.
SearchFragment.java (child fragment of the searchable Activity mentioned above)
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_search, menu);
// Get the searchable.xml data about the search configuration
final SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
SearchableInfo searchInfo = searchManager.getSearchableInfo(getActivity().getComponentName());
// Associate searchable configuration with the SearchView
mSearchView = (SearchView) menu.findItem(R.id.menu_item_search).getActionView();
mSearchView.setSearchableInfo(searchInfo);
mSearchView.requestFocus();
mSearchView.onActionViewExpanded();
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
mSearchListAdapter.clear();
return false;
}
#Override
public boolean onQueryTextChange(String query) {
mSearchListAdapter.clear();
// Execute search ...
return false;
}
});
}
I imagine the work needs to be done within the onQueryTextChange(String query) method above, but I'm not sure what needs to be called. I thought of invoking the SearchManager's startSearch instance method, but that doesn't appear to be best practice. Does anyone have any experience with type-to-search and would be willing to share an efficient solution?
UPDATE:
MainActivity.java (the searchable Activity)
#Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// Handle the search for a particular musical object
final SearchFragment searchFragment = (SearchFragment) getFragmentManager().findFragmentByTag(SearchFragment.TAG);
String query = intent.getStringExtra(SearchManager.QUERY);
mWebService.searchTracks(query, new Callback<Pager>() {
#Override
public void success(Pager results, Response response) {
Log.d(TAG, "Search response received.");
searchFragment.updateItems(results);
}
#Override
public void failure(RetrofitError retrofitError) {
Log.e(TAG, "Search response failed: " + retrofitError.toString());
}
});
The above search interface design is what's recommended by the Android team at Google.
So far, the only solution that I have come across after reading through several pages of documentation is simply sending an intent with the Intent.ACTION_SEARCH action and the current query from the SearchView to start the searchable Activity whenever the SearchView's text changes. Keep in mind that this probably isn't the best practice in terms of the SearchManager design, but it works. I'll revisit this approach at a later date and report back here if I come across anything new.
#Override
public boolean onQueryTextChange(String query) {
mSearchListAdapter.clear();
if (!query.isEmpty()) {
Intent searchIntent = new Intent(getActivity(), MainActivity.class);
searchIntent.setAction(Intent.ACTION_SEARCH);
searchIntent.putExtra(SearchManager.QUERY, query);
startActivity(searchIntent);
}
return false;
}
A TextWatcher should be what you are looking for. It also offers for executing code before or after the text has changed.
http://developer.android.com/reference/android/text/TextWatcher.html
When an object of a type is attached to an Editable, its methods will be called when the text is changed.
This is the perfect solution for the issue you are looking for.
See this this will help you...
Android Action Bar SearchView as Autocomplete?
In case you're still looking for a better solution:
I just found this answer to a similar question, which uses
searchView.setOnQueryTextListener(this);
which is exactly what you want.

Accessibility function implementation problems in Android

I'm developing application that views books. There is a screen (Activity) which shows a book. It has custom view, something similar to ViewSwitcher and every page is a bitmap that is rendered by a custom View.
Now I should implement accessibility function - book should be read by the phone (audio).
I've read Accessibility section here https://developer.android.com/guide/topics/ui/accessibility/index.html but it is not clear enough.
I use SupportLibrary for accessibility management and now I have this code in ViewGroup (which manages book pages). Code 1:
private class EditionPagesViewSwitcherAccessibilityDelegate extends AccessibilityDelegateCompat {
private int mPageCount;
private double[] mPageRange;
#Override
public void onInitializeAccessibilityEvent(final View host, final AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(EditionPagesViewSwitcher.class.getName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
event.setScrollable(canScroll());
}
if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED && updatePageValues()) {
event.setItemCount(mPageCount);
// we use +1 because of user friendly numbers (from 1 not 0)
event.setFromIndex((int) (mPageRange[0] + 1));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
event.setToIndex((int) (mPageRange[1] + 1));
}
}
}
#Override
public void onInitializeAccessibilityNodeInfo(final View host, final AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(EditionPagesViewSwitcher.class.getName());
info.setScrollable(canScroll());
info.setLongClickable(true);
if (canScrollForward()) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
}
if (canScrollBackward()) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
}
}
#Override
public boolean performAccessibilityAction(final View host, final int action, final Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
if (canScrollForward()) {
showNext();
return true;
}
}
return false;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
if (canScrollBackward()) {
showPrevious();
return true;
}
}
return false;
}
return false;
}
Here is code from page view Code 2:
#Override
public void onInitializeAccessibilityEvent(final View host, final AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(EditionPageView.class.getName());
if (hasText()) {
event.getText().add(getPageRangeText());
final String trimText = mSurfaceUpdateData.getPageText().trim();
if (trimText.length() > MAX_TEXT_LENGTH) {
event.getText().add(trimText.substring(0, MAX_TEXT_LENGTH));
// event.getText().add(trimText.substring(MAX_TEXT_LENGTH, trimText.length()));
}
else {
event.getText().add(trimText);
}
}
}
#Override
public void onInitializeAccessibilityNodeInfo(final View host, final AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(EditionPageView.class.getName());
}
Because page text data loads asynchronous first time accessibility don't have any text while executes onInitializeAccessibilityEvent code. And then when data have been loaded I fire AccessibilityEvent.TYPE_VIEW_SELECTED and AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED events. Then onInitializeAccessibilityEvent executes again and phone "read" book text.
So my questions:
Is my Accessibility implementation right? May be it is design wrong? Because I didn't find any good tutorial about this feature.
Why I need to use SDK versions checks in Support implementations in Code 1? Why support implementation doesn't handle it correctly?
Is firing TYPE_VIEW_SELECTED and TYPE_VIEW_TEXT_CHANGED really needed? Or may be some other code should be implemented?
The main question. In Code 2 there is commented code line. This code statement substring text to be less then MAX_TEXT_LENGTH (it's 3800) because if text is bigger nothing is played. Nothing. Is it accessibility restriction? Any other text that is less then this value is played well.
Does anyone know where I can find any good tutorial? (yes I saw samples).
Does anyone have any custom realizations to look through?
UPDATED
Well. Here is some answers:
As I can see TYPE_VIEW_SELECTED and TYPE_VIEW_TEXT_CHANGED events are not needed if you don't want this text to be read as soon as you get it.
On Nexus 7 all large text is played well (text up to 8000 symbols), so this issue doesn't reproduce on it, but on Samsung Galaxy Tab 10.1 (Android 4.0.4) and Genymotion emulator of Tab 10.1 with Android 4.3 does. And this is strange...
4.. According to the documentation of String.substring()
The first argument you pass is the start index in the original string, the second argument is the end index in the original string.
Example:
String text = "Hello";
partOfText = text.substring(2,text.length() - 1);
partOfText equals to "llo" (the first char is index 0)
So by putting your constant MAX_TEXT_LENGTH as a first argument, it would start at index 3800 to take out the substring.
http://developer.android.com/reference/java/lang/String.html#substring(int)
You are right MAX_TEXT_LENGTH is 3800.
About your doubt,
this code:
event.getText().add(trimText.substring(MAX_TEXT_LENGTH, trimText.length()));
}
you are trying to substring "trimText" from MAX_TEXT_LENGTH to trimText.length() !
Supposing that trimText = "STACK", trimText.length() = 5, then trimText.substring(3800,5) is going to be ?
At first, this doesn't have sense, using correctly would be like this:
trimText.substring(0,2) = "ST";

Categories

Resources