Search code examples
androidsearchvoice-recognitionsearchableonnewintent

How to determine if a search was performed through text input or voice recognition?


This will be a self-answered question, because I'd like to clearly document how to determine if a search intent was triggered by text input or voice recognition. My reason for needing this was because I was trying to log searches in my app to Google Analytics, and I needed to know whether the user was typing their searches in on the keyboard as text, or if they were using the voice search feature.

I found a few questions on StackOverflow that addressed this question, but I found them to be hard to find and poorly documented. So hopefully my answer will provide the right keywords and details to help others find this topic more quickly and clearly in the future.


Solution

  • The search Activity in my app is running in singleTop mode, and so I'm handling incoming search intents through an @Override of onNewIntent(). Within this method, the first thing you need to do is check to see if the incoming intent is, in fact, a search intent. We accomplish this in the following way:

    if (intent.getAction().equals(Intent.ACTION_SEARCH)){}

    The next part is a little tricky, and has caused me some false positives on older APIs. There are two extras that come attached to the ACTION_SEARCH intent: QUERY and USER_QUERY. If we read about these two values in the SearchManager docs, we find the following info:

    public static final String QUERY:

    Intent extra data key: Use this key with content.Intent.getStringExtra() to obtain the query string from Intent.ACTION_SEARCH.

    public static final String USER_QUERY:

    Intent extra data key: Use this key with content.Intent.getStringExtra() to obtain the query string typed in by the user. This may be different from the value of QUERY if the intent is the result of selecting a suggestion. In that case, QUERY will contain the value of SUGGEST_COLUMN_QUERY for the suggestion, and USER_QUERY will contain the string typed by the user.

    Essentially what we can gather from these two doc entries is that the value of QUERY will always contain the actual value of the search, regardless of how the search was executed. So it doesn't matter if the user typed in the search, spoke it in with voice recognition, or selected a search suggestion -- this field will contain the raw String value of what they were searching for.

    On the other hand, USER_QUERY may or may not be null, depending on how the search was executed. If the user types in their search using text, this field will contain the value of their search. If the user executes the search with voice recognition, this value will be null.

    This gives us a great (albeit hacky, to some extent) way to determine if the user typed in the search with text or used a voice search feature. But here is the tricky part: as you can see in the documentation above, it is suggested to use getStringExtra() to retrieve this value from the intent. This is not reliable, because the value of USER_QUERY is actually a SpannableString, not a String. This will likely lead to a ClassCastException, because if you use getStringExtra(), you are trying to cast a SpannableString to a String, which will not work. The exception will be automatically caught for you, but you may receive a false-positive null back as the result, leading your code to believe that the search was executed through voice recognition, when in reality the typed-in text value simply got lost because of the ClassCastException.

    So the solution is to instead work with the SpannableString coming from USER_QUERY, and check for a null on that instead so you don't trigger the ClassCastException and the false-positive null. But you'll notice that Intent doesn't have a method to get SpannableStrings, but it does have a getCharSequenceExtra() method. Since SpannableString implements CharSequence, we can simply use this method to retrieve our USER_QUERY SpannableString, and cast it on the way out. So we'll do that like this:

    SpannableString user_query_spannable = (SpannableString) intent.getCharSequenceExtra(SearchManager.USER_QUERY);
    

    We can now check for a null on this value to safely determine if the user typed in text or executed the search in some other way such as voice recognition.

    Putting it all together, we end up with something like this:

    @Override
    protected void onNewIntent(Intent intent) {
    
        if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
    
            SpannableString user_query_spannable = (SpannableString) intent.getCharSequenceExtra(SearchManager.USER_QUERY);
    
            //if this was a voice search
            if (user_query_spannable == null) {
                //Log, send Analytics hit, etc.
                //Then perform search as normal...
                performSearch(intent.getStringExtra(SearchManager.QUERY));
    
            } else {
                //Log, send Analytics hit, etc.
                //Then perform search as normal
                performSearch(intent.getStringExtra(SearchManager.QUERY));
            }
        } 
    }
    

    As I mentioned earlier, for me this information was not easily found through searches on Google and on StackOverflow, especially when the official Android documentation provides misleading information on how to work with these values. Hopefully, this answer will provide a much more easily accessbile source for others looking for similar info.