Question

I have a ListView that I'm populating with information from the media store. I have checkboxes on each row to allow the user to select multiple rows. In the options menu, there are menu items that trigger new queries to the media store and using a CursorLoader i swap the cursor in the adapter when the cursor has loaded.

In my adapter I'm using an ArrayList that keeps track of the checked items and one that keeps track of all items in the list. The list that keeps track of all items needs to be re-sorted/rebuilt when the cursor changes to allow the getView() method to check the correct checkboxes. I have been unable to find a way to update my list so that it corresponds with the order of the items in the cursor.

I've tried overriding swapCursor, changeCursor and notifyDataSetChanged. In each trying to call the super method before and after I resort the list as done in the constructor of the adapter.

I've looked these issues here at SO which seem to be related but I've been unable to create a solution: Problems with Listview adapter

Custom CursorAdapter and CheckBox states

Cursor not binding text correctly with custom adapter

This is my activity code:

/**
 * This activity displays a list of the available media on the device. It allows
 * selecting several items from the list and by selecting the "done" icon in the
 * options menu, the activity will return the results to the calling activity.
 * 
 * The list can be sorted via the options menu. The available sorting columns
 * are artist, title and album. By default the list is sorted by artist name.
 * 
 * The selection from the database consists of the _ID, ARTIST, ALBUM, TITLE,
 * DATA, DISPLAY_NAME and DURATION columns and is also limited to contain only
 * files that are markes as IS_MUSIC.
 * 
 * @author Daniel Kvist
 * 
 */
public class MediaSelectorActivity extends Activity implements LoaderCallbacks<Cursor>
{
    private static final int LOADER_ID_ARTIST = 2;
    private static final int LOADER_ID_ALBUM = 4;
    private static final int LOADER_ID_TITLE = 8;

    public static final String EXTRA_SELECTED_ITEMS = "selected_media";
    public static final int REQUEST_MEDIA = 0;

    private MediaSelectorAdapter adapter;
    private ListView listView;
    private LoaderManager loaderManager;

    private String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
    private String[] projection = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
            MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DURATION };
    private ArrayList<Track> selectedItems;

    /**
     * The onCreate method loads the xml layout which contains the listview. It
     * also gets the loader manager and initiates a first load of available
     * media and sorts it by artist name.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_selector);
        loaderManager = getLoaderManager();
        loaderManager.initLoader(LOADER_ID_ARTIST, null, this);
        listView = (ListView) findViewById(R.id.list);
        selectedItems = new ArrayList<Track>();
    }

    /**
     * This method simply inflates the xml file which contains the menu options.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.media_selector_menu, menu);
        return true;
    }

    /**
     * This is called when an option item has been selected. Depending on the
     * user selection either the selected tracks are passed back to the calling
     * activity or a new query is made to the media store to sort on either
     * artist, album or title.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        selectedItems = adapter.getSelectedItems();
        switch (item.getItemId())
        {
            case R.id.done:
                Intent intent = new Intent();
                intent.putParcelableArrayListExtra(EXTRA_SELECTED_ITEMS, selectedItems);
                setResult(RESULT_OK, intent);
                finish();
                return true;
            case R.id.artist:
                loaderManager.initLoader(LOADER_ID_ARTIST, null, this);
                return true;
            case R.id.album:
                loaderManager.initLoader(LOADER_ID_ALBUM, null, this);
                return true;
            case R.id.track:
                loaderManager.initLoader(LOADER_ID_TITLE, null, this);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     * Called when the cursor loader is first created. It decides which URI to
     * query and which sorting order should be returned. The query also contains
     * information about which columns we are interested in which selection we
     * want.
     */
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle)
    {
        CursorLoader cursorLoader = null;
        switch (i)
        {
            case LOADER_ID_ARTIST:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.ARTIST);
                break;
            case LOADER_ID_ALBUM:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.ALBUM);
                break;
            case LOADER_ID_TITLE:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.TITLE);
                break;
        }
        return cursorLoader;
    }

    /**
     * When the load has finished we create a new adapter of the cursor we
     * receive from the media store content provider. The adapter is then set to
     * the listvew. The adapter uses ARIST, ALBUM and TITLE to be displayed to the
     * user.
     */
    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)
    {
        if(adapter == null) 
        {
            adapter = new MediaSelectorAdapter(getApplicationContext(), R.layout.activity_media_selector, cursor, new String[] {
                MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE }, new int[] { R.id.text_1,
                R.id.text_2, R.id.text_3 }, Adapter.NO_SELECTION, selectedItems);
            listView.setAdapter(adapter);
        }
        else
        {
            adapter.swapCursor(cursor);
        }
    }

    /**
     * WHen the loader is reset we just pass in null as the cursor to the
     * adapter.
     */
    public void onLoaderReset(Loader<Cursor> cursorLoader)
    {
        adapter.swapCursor(null);
    }
}

This is my adapter code:

/**
 * This adapter is used by the media selector activity to display the list rows.
 * It is needed to keep track of which checkboxes have been checked and which
 * has not. The system is aggressive in trying to re-use views that are not
 * currently being displayed which leads to strange behaviour with the
 * checkboxes where they keep their "checked" state although they have not been
 * checked for a specific item.
 * 
 * The class is extending SimpleCursorAdapter for easy use of the cursor that
 * can be obtained from a database or content resolver.
 * 
 * @author Daniel Kvist
 * 
 */
public class MediaSelectorAdapter extends SimpleCursorAdapter
{
    private Context context;
    private ArrayList<Track> listItems;
    private ArrayList<Track> selectedItems;

    /**
     * The constructor takes the same parameters as an ordinary simple cursor
     * adapter and passes them up to the super class. It then loops through the
     * cursor and initiates an array which contains references to all the list
     * rows and if they have been checked or not.
     * 
     * @param context
     *            the context which to be displayed in
     * @param layout
     *            the layout file for the list view
     * @param cursor
     *            the cursor that points to the data
     * @param from
     *            the fields that are to be displayed
     * @param to
     *            the views to display the fields in
     * @param flags
     *            any special flags that can be used to determine the behaviour
     *            of the super class adapter
     * @param selectedItems2 
     */
    public MediaSelectorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags, ArrayList<Track> selectedItems)
    {
        super(context, layout, cursor, from, to, flags);
        this.context = context;
        this.selectedItems = selectedItems;
        listItems = new ArrayList<Track>();

        while (cursor.moveToNext())
        {
            Track track = new Track(cursor.getString(0), cursor.getString(1), cursor.getString(2), cursor.getString(3),
                    cursor.getString(4), cursor.getString(5), cursor.getString(6));
            listItems.add(track);
        }
    }

    /**
     * Overridden method that getView uses to keep track of how many items the
     * adapter has.
     */
    @Override
    public int getCount()
    {
        return listItems.size();
    }

    /**
     * Called by the system to get a specific item.
     */
    @Override
    public Track getItem(int position)
    {
        return listItems.get(position);
    }

    /**
     * Called by the system to get the id/position for an item.
     */
    @Override
    public long getItemId(int position)
    {
        return position;
    }

    /**
     * Reuses old views if they have not already been reset and inflates new
     * views for the rows in the list that needs a new one. It the adds a
     * listener to each checkbox that is used to store information about which
     * checkboxes have been checked or not. Finally we set the checked status of
     * the checkbox and let the super class do it's thing.
     */
    @Override
    public View getView(final int position, View convertView, ViewGroup parent)
    {
        if (convertView == null)
        {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.media_selector_item_layout, null);
        }
        final CheckBox checkBox = (CheckBox) convertView.findViewById(R.id.checkbox);
        checkBox.setOnClickListener(new OnClickListener()
        {
            public void onClick(View v)
            {
                CheckBox cb = (CheckBox) v.findViewById(R.id.checkbox);
                if (cb.isChecked())
                {
                    selectedItems.add(listItems.get(position));
                }
                else if (!cb.isChecked())
                {
                    selectedItems.remove(listItems.get(position));
                }
            }
        });
        // If the selected items contains the current item, set the checkbox to be checked
        checkBox.setChecked(selectedItems.contains(listItems.get(position)));

        return super.getView(position, convertView, parent);
    }

    /**
     * Returns an array list with all the selected items as Track objects.
     * 
     * @return the selected items
     */
    public ArrayList<Track> getSelectedItems()
    {
        return selectedItems;
    }
}

Any hints, tips or other input is greatly appreciated.

Thanks

Was it helpful?

Solution

Ok, so this is what I ended up with. Feel free to use it however you like. The solution is not to use a list at all but rather use the Cursor with getCursor() which is why we use a cursor adapter in the first place.

Activity:

/**
 * This activity displays a list of the available media on the device. It allows
 * selecting several items from the list and by selecting the "done" icon in the
 * options menu, the activity will return the results to the calling activity.
 * 
 * The list can be sorted via the options menu. The available sorting columns
 * are artist, title and album. By default the list is sorted by artist name.
 * 
 * The selection from the database consists of the _ID, ARTIST, ALBUM, TITLE,
 * DATA, DISPLAY_NAME and DURATION columns and is also limited to contain only
 * files that are markes as IS_MUSIC.
 * 
 * @author Daniel Kvist
 * 
 */
public class MediaSelectorActivity extends Activity implements LoaderCallbacks<Cursor>
{
    private static final int LOADER_ID_ARTIST = 2;
    private static final int LOADER_ID_ALBUM = 4;
    private static final int LOADER_ID_TITLE = 8;

    public static final String EXTRA_SELECTED_ITEMS = "selected_media";
    public static final int REQUEST_MEDIA = 0;

    private MediaSelectorAdapter adapter;
    private ListView listView;
    private LoaderManager loaderManager;

    private String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
    private String[] projection = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
            MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DURATION };
    private ArrayList<Track> selectedItems;

    /**
     * The onCreate method loads the xml layout which contains the listview. It
     * also gets the loader manager and initiates a first load of available
     * media and sorts it by artist name.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_selector);
        loaderManager = getLoaderManager();
        loaderManager.initLoader(LOADER_ID_ARTIST, null, this);
        listView = (ListView) findViewById(R.id.list);
        selectedItems = new ArrayList<Track>();
    }

    /**
     * This method simply inflates the xml file which contains the menu options.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.media_selector_menu, menu);
        return true;
    }

    /**
     * This is called when an option item has been selected. Depending on the
     * user selection either the selected tracks are passed back to the calling
     * activity or a new query is made to the media store to sort on either
     * artist, album or title.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        selectedItems = adapter.getSelectedItems();
        switch (item.getItemId())
        {
            case R.id.done:
                Intent intent = new Intent();
                intent.putParcelableArrayListExtra(EXTRA_SELECTED_ITEMS, selectedItems);
                setResult(RESULT_OK, intent);
                finish();
                return true;
            case R.id.artist:
                loaderManager.initLoader(LOADER_ID_ARTIST, null, this);
                return true;
            case R.id.album:
                loaderManager.initLoader(LOADER_ID_ALBUM, null, this);
                return true;
            case R.id.track:
                loaderManager.initLoader(LOADER_ID_TITLE, null, this);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     * Called when the cursor loader is first created. It decides which URI to
     * query and which sorting order should be returned. The query also contains
     * information about which columns we are interested in which selection we
     * want.
     */
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle)
    {
        CursorLoader cursorLoader = null;
        switch (i)
        {
            case LOADER_ID_ARTIST:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.ARTIST);
                break;
            case LOADER_ID_ALBUM:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.ALBUM);
                break;
            case LOADER_ID_TITLE:
                cursorLoader = new CursorLoader(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
                        MediaStore.Audio.Media.TITLE);
                break;
        }
        return cursorLoader;
    }

    /**
     * When the load has finished we create a new adapter of the cursor we
     * receive from the media store content provider. The adapter is then set to
     * the listvew. The adapter uses ARIST, ALBUM and TITLE to be displayed to the
     * user.
     */
    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)
    {
        if(adapter == null) 
        {
            adapter = new MediaSelectorAdapter(getApplicationContext(), R.layout.activity_media_selector, cursor, new String[] {
                MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE }, new int[] { R.id.text_1,
                R.id.text_2, R.id.text_3 }, Adapter.NO_SELECTION, selectedItems);
            listView.setAdapter(adapter);
        }
        else
        {
            adapter.swapCursor(cursor);
        }
    }

    /**
     * WHen the loader is reset we just pass in null as the cursor to the
     * adapter.
     */
    public void onLoaderReset(Loader<Cursor> cursorLoader)
    {
        adapter.swapCursor(null);
    }
}

Adapter:

/**
 * This adapter is used by the media selector activity to display the list rows.
 * It is needed to keep track of which checkboxes have been checked and which
 * has not. The system is aggressive in trying to re-use views that are not
 * currently being displayed which leads to strange behaviour with the
 * checkboxes where they keep their "checked" state although they have not been
 * checked for a specific item.
 * 
 * The class is extending SimpleCursorAdapter for easy use of the cursor that
 * can be obtained from a database or content resolver.
 * 
 * @author Daniel Kvist
 * 
 */
public class MediaSelectorAdapter extends SimpleCursorAdapter
{
    private Context context;
    private ArrayList<Track> selectedItems;

    /**
     * The constructor takes the same parameters as an ordinary simple cursor
     * adapter and passes them up to the super class. It then loops through the
     * cursor and initiates an array which contains references to all the list
     * rows and if they have been checked or not.
     * 
     * @param context
     *            the context which to be displayed in
     * @param layout
     *            the layout file for the list view
     * @param cursor
     *            the cursor that points to the data
     * @param from
     *            the fields that are to be displayed
     * @param to
     *            the views to display the fields in
     * @param flags
     *            any special flags that can be used to determine the behaviour
     *            of the super class adapter
     * @param selectedItems2 
     */
    public MediaSelectorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags, ArrayList<Track> selectedItems)
    {
        super(context, layout, cursor, from, to, flags);
        this.context = context;
        this.selectedItems = selectedItems;
    }

    /**
     * Reuses old views if they have not already been reset and inflates new
     * views for the rows in the list that needs a new one. It the adds a
     * listener to each checkbox that is used to store information about which
     * checkboxes have been checked or not. Finally we set the checked status of
     * the checkbox and let the super class do it's thing.
     */
    @Override
    public View getView(final int position, View convertView, ViewGroup parent)
    {
        Cursor c = getCursor();
        c.moveToPosition(position);
        final Track track = new Track(c.getString(0), c.getString(1), c.getString(2), c.getString(3),
                c.getString(4), c.getString(5), c.getString(6));

        if (convertView == null)
        {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.media_selector_item_layout, null);
        }
        final CheckBox checkBox = (CheckBox) convertView.findViewById(R.id.checkbox);
        checkBox.setOnClickListener(new OnClickListener()
        {
            public void onClick(View v)
            {
                CheckBox cb = (CheckBox) v.findViewById(R.id.checkbox);
                if (cb.isChecked())
                {
                    selectedItems.add(track);
                }
                else if (!cb.isChecked())
                {
                    selectedItems.remove(track);
                }
            }
        });
        // If the selected items contains the current item, set the checkbox to be checked
        checkBox.setChecked(selectedItems.contains(track));
        return super.getView(position, convertView, parent);
    }

    /**
     * Returns an array list with all the selected items as Track objects.
     * 
     * @return the selected items
     */
    public ArrayList<Track> getSelectedItems()
    {
        return selectedItems;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top