· 5 min read
Use Voice Search to integrate with Google Now
One of the awesome things about Android is the ability we have to integrate our apps into the ecosystem in many different ways. Apps can talk among each others, and this fact gives us an impressive flexibility to create unique experiences.
Integrations with Google are a clear example. We have a bunch of different features that can help our App improve its visibility, such as App Indexing or a powerful set of Voice Actions.
Voice Search
In this article, I’ll focus on how we can implement Voice Search in our Apps, though the process will be similar for any other voice actions. Voice search will allow us to say Google Now to search something inside our App. You can try this on Play Music for instance:
OK Google, Search for The Beatles on Play Music
This command will open a search inside Play Music App looking for The Beatles.
How to implement Voice Search
When the Voice Search is launched, our App will receive an intent with the query text, that we must capture and analyze. So the first part consists of specifying which activity will receive the intent:
<activity
android:name=".MainActivity"
android:launchMode="singleTask" >
<intent-filter>
<action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
The action name is com.google.android.gms.actions.SEARCH_ACTION
, so we say our MainActivity
to handle this kind of intents. Besides, I’m using a singleTask
launch mode, so that the MainActivity
is only created once. Otherwise, a new activity instance would be created every time we receive the intent.
The next step is to deal with it inside the MainActivity
. As we are using singleTask
mode, the intent can be received from two different points of the activity: the first time the activity is created, by using getIntent()
, and in onNewIntent
method. So we are creating a handler method that can be called whenever we need it:
private static final String ACTION_VOICE_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION";
private void handleVoiceSearch(Intent intent) {
if (intent != null && ACTION_VOICE_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
setSearchViewVisible(true);
searchView.setQuery(query, true);
}
}
This method will check that the intent is not null and the action is the one we’re trying to detect before getting the query text, which will be an extra inside the intent. The key for the query extra is SearchManager.QUERY
.
After that I’m showing the SearchView
, setting the query and submitting so that the query is performed. This method is called in onNewIntent
:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleVoiceSearch(intent);
}
As well as when the UI is ready (in our case, when the menu is inflated and we have access to the SearchView
), as you’ll see later.
In my example, the UI is based on a SearchView
inside the Toolbar. You can see how to implement the SearchView
in a previous article, but I’ll explain a little bit how to do it.
First, create menu action:
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@drawable/ic_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom" />
</menu>
And then, when inflating the menu, you can request the SearchView
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
searchView.setOnSearchClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setSearchViewVisible(true);
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Toast.makeText(MainActivity.this, query, Toast.LENGTH_LONG).show();
searchView.clearFocus();
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
handleVoiceSearch(getIntent());
return true;
}
private void setSearchViewVisible(boolean visible) {
if (searchView.isIconified() == visible) {
searchView.setIconified(!visible);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(visible);
}
}
How to try it
We can’t try this example directly from Google Now, because the App needs to be published into the Play Store so that Google Now can detect it. But we have an alternative to debug it by using ADB
. This is the command you need:
adb shell am start -a com.google.android.gms.actions.SEARCH_ACTION -e query searchquery app_package
Using the example you can download from my repository, we could do:
adb shell am start -a com.google.android.gms.actions.SEARCH_ACTION -e query VoiceSearch com.antonioleiva.googlenowsearch
You can try the example both when the App is totally closed and when it’s open. That way you’ll test both possible paths.
Extra: Kotlin implementation
As you may know, I’m talking a lot about Kotlin these days, because I think it’s a very good alternative to Java, and it let us write a simpler and more readable code. As an example, I’m going to simplify onCreateOptionsMenu
, but you can find the complete code in this same repository.
The ability to implement extension functions can help us reduce the verbosity. For instance, we can create an extension function for Menu
s that finds the ActionView
based on the action id and returns a casted view:
inline fun <reified T : View?> Menu.findCompatActionView(actionRes: Int): T {
val searchItem = findItem(actionRes)
return MenuItemCompat.getActionView(searchItem) as T
}
You can now do:
searchView = menu.findCompatActionView(R.id.action_search)
Another extension function could help us write the QueryTextListener
in a cleaner way:
fun SearchView.onQueryText(submit: (String) -> Boolean = { false }, textChange: (String) -> Boolean = { false }) {
this.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean = submit(query)
override fun onQueryTextChange(newText: String): Boolean = textChange(newText)
})
}
This function receives a couple of functions, one for each method in the listener, and gives them a default value. This way we only need to define the ones we’re going to use. As we only want the first function (we can use the default for the second one), now we just do:
searchView.onQueryText ({ longToast(it) searchView.clearFocus() true })
This is how the function finally looks:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main, menu)
searchView = menu.findCompatActionView(R.id.action_search)
searchView.setOnSearchClickListener { setSearchViewVisible(true) }
searchView.onQueryText ({ longToast(it) searchView.clearFocus() true })
intent?.let { handleVoiceSearch(it) }
return true
}
As you can see, as intent
can be null at this point (for instance, on a regular activity creation), we must check its nullity before using it. We can prevent the creation of a conditional if
, just by using the function let
, which will only get into the body if the object that called it is not null.
If you’re interested in Kotlin, you can take a look at Kotlin articles or get Kotlin for Android Developers book I’m writing.