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:
[xml]
<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>
[/xml]
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:
[java]
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);
}
}
[/java]
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
:
[java]
@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleVoiceSearch(intent);
}
[/java]
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:
[xml]
<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>
[/xml]
And then, when inflating the menu, you can request the SearchView
:
[java]
@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);
}
}
[/java]
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:
[kotlin]
inline fun <reified T : View?> Menu.findCompatActionView(actionRes: Int): T {
val searchItem = findItem(actionRes)
return MenuItemCompat.getActionView(searchItem) as T
}
[/kotlin]
You can now do:
[kotlin]
searchView = menu.findCompatActionView(R.id.action_search)
[/kotlin]
Another extension function could help us write the QueryTextListener
in a cleaner way:
[kotlin]
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)
})
}
[/kotlin]
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:
[kotlin]
searchView.onQueryText ({
longToast(it)
searchView.clearFocus()
true
})
[/kotlin]
This is how the function finally looks:
[kotlin]
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
}
[/kotlin]
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.
Do you know how to change the name of your app for voice commands, without changing the actual name?
I’d say it’s not possible.
First of all i would like to thank Antonio for this beautiful tutorial on voice commands.
Other than that, i believe i have found a way for this particular problem that neotechni is facing. I have solved it using an extra starter activity that has the sole purpose to start my main activity and this extra activity has its android:label property in manifest is set to my custom name. With this way you can call this starter activity with your custom name without changing the actual name of the app.
@Aydin Emre Iskender, could you share your code for the fix to the problem that neotechni is facing. TIA
Can we send action to our application on the basis of some predefined/custom keyword else it should not invoke an application?
No, you receive the broadcast an can choose to do nothing with it
Antonio, I was try to test using adb as suggested from your article.
Your suggested adb command: does one need to add paths to (the directory) where the VoiceSearch app is located? I tried copying the apk into the adb folder before issuing the command but kept receiving error
I am able to build the app successfully but am having a tough time testing it from adb
No, what you set there is the package of the app, you don’t need the apk file anywhere. Check that in my case it was com.antonioleiva.googlenowsearch, because it was the package I used to identify the App.
Hi Antonio,
Thank you for the article. With Goolge pushing Feed and Assistant, is this method still vaild. I am having trouble finding out any information.
Thanks
AFAIK for the basic part, it’s still the same. This code will still let Assistant run a search query into your App. But haven’t done many tests recently so can’t confirm 100%.
No worries, it will be worth trying. Thank you very much for the reply
Have implemented the same search functionality in my xamarin App. But i am not getting into my app from Google Now/Google Assistant (even after published my app).
Note: It’s working on some devices but not all.
How to interact with app, if we want app to reply back to user query and further continue the conversation? I have tried https://developers.google.com/voice-actions/interaction/voice-interactions, but isVoiceInteraction in onResume always returns false. Can we help me with this?
Sorry, I haven’t done much more related to this, apart from to what you see here…