Android – SearchView using Cursor Loader

A little background about my app.
My main activity has 3 tabs which are fragments. Tabs 2-3 are doing nothing at the moment. Tab 1 is currently populated using a listview and cursor loader. It displays a list of ingredients correctly. Now I’m trying to add search functionality since there will be too many ingredients to scroll through.

My goal
I’ve been digging up tutorials and posts from this site for days and I just can’t seem to figure out how to connect my searchview to my data to allow users to search by ingredient name and select the ingredient they find. Any help and advice you can provide is much appreciated!

I’ve followed the Android Developer documentation and stuck at this point:

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // do some stuff here but its not clear what.
}

private fun handleIntent(intent: Intent) {
    if (Intent.ACTION_SEARCH == intent.action) {
        val query = intent.getStringExtra(SearchManager.QUERY)
        // do some stuff here but its not clear what.
    }
}

My Main Activity (CatalogActivity)

import android.app.SearchManager
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.SearchView
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import kotlinx.android.synthetic.main.activity_catalog.*
import onCreateDesigns.com.CraftCocktailRecipes.data.DbContract.IngredientEntry

class CatalogActivity : AppCompatActivity() {

private lateinit var mSectionsPagerAdapter: SectionsPagerAdapter

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_catalog)

    setSupportActionBar(toolbar)

    // Create the adapter that will return a fragment for each of the three
    // primary sections of the activity.
    mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)

    // Set up the ViewPager with the fragment adapter.
    view_pager_container.adapter = mSectionsPagerAdapter

    // Set the number of pages that should be retained to either side of the current page (tab)
    // in the view hierarchy in an idle state.
    view_pager_container.offscreenPageLimit = 2

    // Sets the tab that opens first when the app is started.
    //view_pager_container.currentItem = 0

    view_pager_container.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
    tabs.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(view_pager_container))

    // Setup FAB to open EditorActivity
    fab_button.setOnClickListener {
        val intent = Intent([email protected], EditorActivity::class.java)
        startActivity(intent)
    }

    handleIntent(intent)

}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
}

private fun handleIntent(intent: Intent) {

    if (Intent.ACTION_SEARCH == intent.action) {
        val query = intent.getStringExtra(SearchManager.QUERY)


    }
}


/**
 * Helper method to insert hardcoded ingredient data into the database. For debugging purposes only.
 */
private fun insertIngredient() {
    // Create a ContentValues object where column names are the keys,
    // and Rittenhouse Rye Whiskey attributes are the values.
    val values = ContentValues()
    values.put(IngredientEntry.COLUMN_INGREDIENT_NAME, "Rittenhouse Rye")
    values.put(IngredientEntry.COLUMN_INGREDIENT_DESCRIPTION, "Earthy with a sweet finish.")
    values.put(IngredientEntry.COLUMN_INGREDIENT_CATEGORY, IngredientEntry.CATEGORY_WHISKEY)
    values.put(IngredientEntry.COLUMN_INGREDIENT_MEASUREMENT, IngredientEntry.MEASUREMENT_OZ)

    // Insert a new row for Rittenhouse Rye Whiskey into the provider using the ContentResolver.
    // Use the {@link IngredientEntry#CONTENT_URI} to indicate that we want to insert
    // into the ingredients database table.
    // Receive the new content URI that will allow us to access Rittenhouse's data in the future.
    contentResolver.insert(IngredientEntry.CONTENT_URI, values)
}

/**
 * Helper method to delete all ingredients in the database. For debugging purposes only.
 */
private fun deleteAllIngredients() {
    val rowsDeleted = contentResolver.delete(IngredientEntry.CONTENT_URI, null, null)
    Log.v("CatalogActivity", rowsDeleted.toString() + " rows deleted from ingredient database")
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    // Inflate the menu options from the res/menu/menu_catalog.xml file.
    // This adds menu items to the app bar.
    menuInflater.inflate(R.menu.menu_catalog, menu)

    // Associate searchable configuration with the SearchView
    val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
    val searchView = menu.findItem(R.id.search).actionView as SearchView
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(componentName))


    return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    // User clicked on a menu option in the app bar overflow menu
    when (item.itemId) {
    // Respond to a click on the "Insert dummy data" menu option
        R.id.action_insert_dummy_data -> {
            insertIngredient()
            return true
        }
    // Respond to a click on the "Delete all entries" menu option
        R.id.action_delete_all_entries -> {
            deleteAllIngredients()
            return true
        }
    }
    return super.onOptionsItemSelected(item)
}
}

My DbContentProvider

import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.util.Log
import onCreateDesigns.com.CraftCocktailRecipes.data.DbContract.IngredientEntry
import kotlin.properties.Delegates

class DbContentProvider : ContentProvider() {

// Database helper object
private var mDbHelper: DbHelper by Delegates.notNull()

// Order the of ingredients in the list view
private val ingredientSortBy = IngredientEntry.COLUMN_INGREDIENT_NAME

override fun onCreate(): Boolean {
    mDbHelper = DbHelper(context)
    return true
}

override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?,
                   sortOrder: String?): Cursor? {

    // Get readable database
    val database: SQLiteDatabase = mDbHelper.readableDatabase

    // This cursor will hold the result of the query
    val cursor: Cursor

    // Figure out if the URI matcher can match the URI to a specific code
    val match = sUriMatcher.match(uri)
    when (match) {
        INGREDIENTS ->
            // For the INGREDIENTS code, query the ingredients table directly with the given
            // projection, selection, selection arguments, and sort order. The cursor
            // could contain multiple rows of the ingredients table.
            // Query is uri, projection, selection, selectionArgs, sortOrder and orderBy
            cursor = database.query(IngredientEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, ingredientSortBy)
        INGREDIENTS_ID -> {
            // For the INGREDIENTS_ID code, extract out the ID from the URI.
            // For an example URI such as "content://onCreateDesigns.com.CraftCocktailRecipes.ingredients/ingredients/3",
            // the selection will be "_id=?" and the selection argument will be a
            // String array containing the actual ID of 3 in this case.
            //
            // For every "?" in the selection, we need to have an element in the selection
            // arguments that will fill in the "?". Since we have 1 question mark in the
            // selection, we have 1 String in the selection arguments' String array.
            val mSelection = IngredientEntry._ID + "=?"
            val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())

            // This will perform a query on the ingredients table where the _id equals 3 to return a
            // Cursor containing that row of the table.
            // Query is uri, projection, selection, selectionArgs, sortOrder and orderBy
            cursor = database.query(IngredientEntry.TABLE_NAME, projection, mSelection, mSelectionArgs, null, null, ingredientSortBy)
        }
        else -> throw IllegalArgumentException("Cannot query unknown URI " + uri)
    }

    // Set notification URI on the Cursor,
    // so we know what content URI the Cursor was created for.
    // If the data at this URI changes, then we know we need to update the Cursor.
    cursor.setNotificationUri(context.contentResolver, uri)

    // Return the cursor
    return cursor
}

override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
    val match = sUriMatcher.match(uri)
    when (match) {
        INGREDIENTS -> return insertIngredient(uri, contentValues)
        else -> throw IllegalArgumentException("Insertion is not supported for " + uri)
    }
}

/**
 * Insert an ingredient into the database with the given content values. Return the new content URI
 * for that specific row in the database.
 */
private fun insertIngredient(uri: Uri, contentValues: ContentValues?): Uri? {
    // Check that the name is not null or empty
    val ingredientName = contentValues?.getAsString(IngredientEntry.COLUMN_INGREDIENT_NAME)
    if (ingredientName.isNullOrEmpty()) {
        throw IllegalArgumentException("Ingredient requires a name")
    }

    // If the {@link IngredientEntry#COLUMN_INGREDIENT_CATEGORY} key is present,
    // check that the category value is valid.
    val category = contentValues?.getAsInteger(IngredientEntry.COLUMN_INGREDIENT_CATEGORY)
    if (category == null || !IngredientEntry.isValidCategory(category)) {
        throw IllegalArgumentException("Ingredient requires valid category")

    }

    // If the {@link IngredientEntry#COLUMN_INGREDIENT_MEASUREMENT} key is present,
    // check that the measurement value is valid.
    val measurement = contentValues.getAsInteger(IngredientEntry.COLUMN_INGREDIENT_MEASUREMENT)
    if (measurement == null || !IngredientEntry.isValidMeasurement(measurement)) {
        throw IllegalArgumentException("Ingredient requires valid measurement")
    }

    // No need to check the description, any value is valid (including null).

    // Get writable database
    val database = mDbHelper.writableDatabase

    // Insert the new ingredient with the given values
    val id = database.insert(IngredientEntry.TABLE_NAME, null, contentValues)
    // If the ID is -1, then the insertion failed. Log an error and return null.
    if (id == (-1).toLong()) {
        Log.e(LOG_TAG, "Failed to insert row for " + uri)
        return null
    }

    // Notify all listeners that the data has changed for the ingredient content URI
    context.contentResolver.notifyChange(uri, null)

    // Return the new URI with the ID (of the newly inserted row) appended at the end
    return ContentUris.withAppendedId(uri, id)
}

override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
                    selectionArgs: Array<String>?): Int {

    val match = sUriMatcher.match(uri)
    return when (match) {
        INGREDIENTS -> updateIngredient(uri, contentValues, selection, selectionArgs)
        INGREDIENTS_ID -> {
            // For the INGREDIENTS_ID code, extract out the ID from the URI,
            // so we know which row to update. Selection will be "_id=?" and selection
            // arguments will be a String array containing the actual ID.
            val mSelection = IngredientEntry._ID + "=?"
            val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())
            updateIngredient(uri, contentValues, mSelection, mSelectionArgs)
        }
        else -> throw IllegalArgumentException("Update is not supported for " + uri)
    }
}

/**
 * Update ingredients in the database with the given content contentValues. Apply the changes to the rows
 * specified in the selection and selection arguments (which could be 0 or 1 or more ingredients).
 * Return the number of rows that were successfully updated.
 */
private fun updateIngredient(uri: Uri, contentValues: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
    // Check that the name is not null or empty
    val ingredientName = contentValues?.getAsString(IngredientEntry.COLUMN_INGREDIENT_NAME)
    if (ingredientName.isNullOrEmpty()) {
        throw IllegalArgumentException("Ingredient requires a name")
    }

    // If the {@link IngredientEntry#COLUMN_INGREDIENT_CATEGORY} key is present,
    // check that the category value is valid.
    val category = contentValues?.getAsInteger(IngredientEntry.COLUMN_INGREDIENT_CATEGORY)
    if (category == null || !IngredientEntry.isValidCategory(category)) {
        throw IllegalArgumentException("Ingredient requires valid category")

    }

    // If the {@link IngredientEntry#COLUMN_INGREDIENT_MEASUREMENT} key is present,
    // check that the measurement value is valid.
    val measurement = contentValues.getAsInteger(IngredientEntry.COLUMN_INGREDIENT_MEASUREMENT)
    if (measurement == null || !IngredientEntry.isValidMeasurement(measurement)) {
        throw IllegalArgumentException("Ingredient requires valid measurement")
    }

    // No need to check the description, any value is valid (including null).

    // If there are no contentValues to update, then don't try to update the database
    if (contentValues.size() == 0) {
        return 0
    }

    // Otherwise, get writable database to update the data
    val database = mDbHelper.writableDatabase

    // Perform the update on the database and get the number of rows affected
    val rowsUpdated = database.update(IngredientEntry.TABLE_NAME, contentValues, selection, selectionArgs)

    // If 1 or more rows were updated, then notify all listeners that the data at the
    // given URI has changed
    if (rowsUpdated != 0) {
        context.contentResolver.notifyChange(uri, null)
    }

    // Return the number of rows updated
    return rowsUpdated
}

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {

    // Get writable database
    val database = mDbHelper.writableDatabase

    // Track the number of rows that were deleted
    val rowsDeleted: Int

    val match = sUriMatcher.match(uri)
    rowsDeleted = when (match) {
        INGREDIENTS ->
            // Delete all rows that match the selection and selection args
            database.delete(IngredientEntry.TABLE_NAME, selection, selectionArgs)
        INGREDIENTS_ID -> {
            // Delete a single row given by the ID in the URI
            val mSelection = IngredientEntry._ID + "=?"
            val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())
            database.delete(IngredientEntry.TABLE_NAME, mSelection, mSelectionArgs)
        }
        else -> throw IllegalArgumentException("Deletion is not supported for " + uri)
    }

    // If 1 or more rows were deleted, then notify all listeners that the data at the
    // given URI has changed
    if (rowsDeleted != 0) {
        context.contentResolver.notifyChange(uri, null)
    }

    // Return the number of rows deleted
    return rowsDeleted
}

override fun getType(uri: Uri): String? {
    val match = sUriMatcher.match(uri)
    return when (match) {
        INGREDIENTS -> IngredientEntry.CONTENT_LIST_TYPE
        INGREDIENTS_ID -> IngredientEntry.CONTENT_ITEM_TYPE
        else -> throw IllegalStateException("Unknown URI $uri with match $match")
    }
}

companion object {

    /** Tag for the log messages  */
    val LOG_TAG: String? = ContentProvider::class.java.simpleName

    /** URI matcher code for the content URI for the ingredients table  */
    private val INGREDIENTS = 100

    /** URI matcher code for the content URI for a single ingredient in the ingredients table  */
    private val INGREDIENTS_ID = 101

    /**
     * UriMatcher object to match a content URI to a corresponding code.
     * The input passed into the constructor represents the code to return for the root URI.
     * It's common to use NO_MATCH as the input for this case.
     */
    private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)

    // Static initializer. This is run the first time anything is called from this class.
    init {
        // The calls to addURI() go here, for all of the content URI patterns that the provider
        // should recognize. All paths added to the UriMatcher have a corresponding code to return
        // when a match is found.

        // The content URI of the form "content://com.example.android.ingredients/ingredients" will map to the
        // integer code {@link #INGREDIENTS}. This URI is used to provide access to MULTIPLE rows
        // of the ingredients table.
        sUriMatcher.addURI(DbContract.CONTENT_AUTHORITY, DbContract.PATH_INGREDIENTS, INGREDIENTS)

        // The content URI of the form "content://com.example.android.ingredients/ingredients/#" will map to the
        // integer code {@link #INGREDIENTS_ID}. This URI is used to provide access to ONE single row
        // of the ingredients table.
        //
        // In this case, the "#" wildcard is used where "#" can be substituted for an integer.
        // For example, "content://com.example.android.ingredients/ingredients/3" matches, but
        // "content://com.example.android.ingredients/ingredients" (without a number at the end) doesn't match.
        sUriMatcher.addURI(DbContract.CONTENT_AUTHORITY, DbContract.PATH_INGREDIENTS + "/#", INGREDIENTS_ID)
    }
}
}

My Manifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="onCreateDesigns.com.CraftCocktailRecipes">

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
        android:name=".CatalogActivity"
        android:label="@string/app_name"
        android:launchMode="singleTop"
        android:theme="@style/AppTheme.NoActionBar">
        <intent-filter>
            <action android:name="android.intent.action.SEARCH" />
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <meta-data
            android:name="android.app.searchable"
            android:resource="@xml/searchable" />
    </activity>
    <activity
        android:name=".EditorActivity"
        android:parentActivityName=".CatalogActivity"
        android:theme="@style/EditorTheme">
        <!-- Parent activity meta-data to support 4.0 and lower -->
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value=".CatalogActivity" />
    </activity>

    <provider
        android:name=".data.DbContentProvider"
        android:authorities="onCreateDesigns.com.CraftCocktailRecipes"
        android:exported="false" />
</application>