Android app crashes when filtering list via Google Voice Actions search action

447 views Asked by At

I am having a problem filtering a list using JSON data parsed via Volley based on this tutorial, when the list is filtered via the Search in App intent in Google's System Voice Actions from the Google App.

The exact problem encountered is listed below:

  1. The app is initially not running at all (the search works perfectly if the app is running or in the background).

  2. Fire the intent via adb:

cd C:...\android-sdk\platform-tools
adb shell am start -a "com.google.android.gms.actions.SEARCH_ACTION" --es query "[query keyword]" -n "com.testapp/.MainActivity"

  1. The correct app opens but the list is empty, i.e. no results filtered.

  2. The app then crashes.

Below is the stacktrace:

02-15 17:54:03.331: D/AndroidRuntime(31982): Shutting down VM
02-15 17:54:03.341: E/AndroidRuntime(31982): FATAL EXCEPTION: main
02-15 17:54:03.341: E/AndroidRuntime(31982): Process: com.test.app, PID: 31982
02-15 17:54:03.341: E/AndroidRuntime(31982): java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
02-15 17:54:03.341: E/AndroidRuntime(31982):    at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:255)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at java.util.ArrayList.get(ArrayList.java:308)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.test.app.adapter.CustomListAdapter.getView(CustomListAdapter.java:92)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.test.app$1.onResponse(MainActivity.java:152)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.test.app$1.onResponse(MainActivity.java:1)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.android.volley.toolbox.JsonRequest.deliverResponse(JsonRequest.java:65)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:99)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.os.Handler.handleCallback(Handler.java:739)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.os.Handler.dispatchMessage(Handler.java:95)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.os.Looper.loop(Looper.java:145)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at android.app.ActivityThread.main(ActivityThread.java:6843)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at java.lang.reflect.Method.invoke(Native Method)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at java.lang.reflect.Method.invoke(Method.java:372)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
02-15 17:54:03.341: E/AndroidRuntime(31982):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)

My app includes a MainActivity which is the searchable activity that displays the list, a model class, an adapter class, and a controller class for the list items (similar to existing Android ListView tutorials).

Below is the code (some code omitted):

MainActivity

//[…]
public class MainActivity extends AppCompatActivity {
    //[…]
    private List<Item> itemList = new ArrayList<Item>();
    public static ListView listView;
    private CustomListAdapter adapter;
    private static final String GMS_SEARCH_ACTION = "com.google.android.gms.actions.SEARCH_ACTION";
    private String qq;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //[…]
        onNewIntent(getIntent());
    }
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        String action = intent.getAction();
        if (action!= null && (action.equals(Intent.ACTION_SEARCH)||action.equals(GMS_SEARCH_ACTION))) {
            qq = intent.getStringExtra(SearchManager.QUERY);
            doSearch(qq);
        }
    }
    /* FIXME - Current problem: From adb command, ListView can only be filtered successfully only if app is still running in background (i.e. onPause()). ListView filtering works ok if searching within the app. If app is not running, sending the search command causes the app to open but nothing is filtered, and then crashes shortly after.*/
    private void doSearch(String qq) {
        CharSequence query = qq.toUpperCase(Locale.getDefault());
        MainActivity.this.adapter.getFilter().filter(query);
        }
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        // […]
        MenuItem searchItem = menu.findItem(R.id.menu_search);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
        searchView.setSearchableInfo(searchableInfo);
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String qq) {
                doSearch(qq);
                return false;
            }
            @Override
            public boolean onQueryTextChange(String qq) {
                doSearch(qq);
                return false;
            }
        });
        return super.onCreateOptionsMenu(menu);
    }
}

Adapter class

// […]
public class CustomListAdapter extends BaseAdapter {
    private Activity activity;
    private LayoutInflater inflater;
    private List<Item> items, default_items;
    private Filter myFilter;
    ImageLoader imageLoader = AppController.getInstance().getImageLoader();
    public CustomListAdapter(Activity activity, List<Item> items) {
        this.activity = activity;
        this.items = items;
        this.default_items = items;
    }
    //[…]
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
//…
    }
    public Filter getFilter() {
        if (myFilter == null) { myFilter = new MYFilter();}
        return myFilter;
    }
    private class MYFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence query) {
            FilterResults results = new FilterResults();
            // If no filter implemented, return the whole list
            if (query == null || query.length() == 0) {
                results.values = default_ items;
                results.count = default_ items.size();
            }
            else {
                List<Item> nitems = new ArrayList<Item>();
                items = default_nitems;
                for (Item t : items) {
                    // Filter logic implemented here, items are added to nitems if it meets conditions
}
                results.values = nitems;
                results.count = nitems.size();
            }
            return results;
        }
        @Override
        protected void publishResults(CharSequence query, FilterResults results) {
            if (results.count == 0){
                items = (List<Item>) results.values;
                MainActivity.emptyView.setVisibility(View.VISIBLE);
                MainActivity.listView.setVisibility(View.GONE);
            }
            else {
                items = (List< Item >) results.values;
                MainActivity.listView.setVisibility(View.VISIBLE);
                MainActivity.emptyView.setVisibility(View.GONE);
                notifyDataSetChanged();
            }
        }
    }
}

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >

    <application
        android:name=".app.AppController"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" android:allowTaskReparenting="true">

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait"
            android:exported="true"
            android:launchMode="singleTop" >

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>

            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable" />

            <intent-filter>
                <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>

            <meta-data
                android:name="android.app.default_searchable"
                android:value=".MainActivity" />

        </activity>
<!-- ... -->
</application>
</manifest>

Any assistance to fix this problem is appreciated.

1

There are 1 answers

0
jackson95 On BEST ANSWER

In case anybody in the future would come across by a similar problem using a ListView based on a dataset populated in another thread or AsyncTask (e.g. JSON parsing via Volley in this tutorial), this problem is caused because the JSON parsing is done concurrently with the onNewIntent(getIntent()); method. Therefore, there may only an empty ArrayList to filter during filtering operation MYFilter which results in the IndexOutOfBoundsException.

In this specific case (using Volley), the solution would be to call onNewIntent(getIntent()); at the end of the onResponse method (refer to this SO question for more information). For me there was no more code after my Volley Request within onCreate() so I'm safe.

I realised that this was problem because since this question was posted, I had created an "offline mode" for the app which does a simple JSON parsing within the main thread, and the filtering via Google Voice Actions works perfectly. Also, I happened to have tested this code on multiple devices, some of which finished parsing the JSON data before receiving the Search in App (com.google.android.gms.actions.SEARCH_ACTION) intent, whilst others did not.