Search code examples
androidloaderandroid-cursorloader

CursorLoader does not refresh data the second time


I'm using three ContentProviders methods: bulkInsert(), query() and delete() (for deleting old data). This is my ContentProvider class:

public class MoviesProvider extends ContentProvider {
    private static final String LOG_TAG = MoviesProvider.class.getSimpleName();

    // The URI Matcher used by this content provider.
    private static final UriMatcher sUriMatcher = buildUriMatcher();
    private MoviesDbHelper mOpenHelper;

    static final int MOVIES = 100;
    static final int MOVIE_ID = 101;

    static UriMatcher buildUriMatcher() {
        // 1) The code passed into the constructor represents the code to return for the root
        // URI.  It's common to use NO_MATCH as the code for this case. Add the constructor below.
        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
        final String authority = CONTENT_AUTHORITY;

        // 2) Use the addURI function to match each of the types.
        matcher.addURI(authority, PATH_MOVIES, MOVIES);
        matcher.addURI(authority, PATH_MOVIES + "/#", MOVIE_ID);

        return matcher;
    }

    @Override
    public boolean onCreate() {
        mOpenHelper = new MoviesDbHelper(getContext());
        return true;
    }


    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // Here's the switch statement that, given a URI, will determine what kind of request it is,
        // and query the database accordingly.
        Cursor retCursor;
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(MoviesEntry.TABLE_NAME);
        switch (sUriMatcher.match(uri)) {
            case MOVIE_ID:
            {
                queryBuilder.appendWhere(MoviesEntry._ID + "=" + uri.getLastPathSegment());
                break;
            }
            case MOVIES:
            {
                break;
            }
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);

        }

        retCursor = queryBuilder.query(mOpenHelper.getReadableDatabase(),
                projection, selection, selectionArgs, null, null, sortOrder);

        retCursor.setNotificationUri(getContext().getContentResolver(), uri);

        return retCursor;
    }

    @Override
    public String getType(Uri uri) {
        // Use the Uri Matcher to determine what kind of URI this is.
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case MOVIES:
                return MoviesEntry.CONTENT_TYPE;
            case MOVIE_ID:
                return MoviesEntry.CONTENT_ITEM_TYPE;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        final int match = sUriMatcher.match(uri);
        Uri returnUri;

        switch (match) {
            case MOVIES: {
                long _id = db.insert(MoviesEntry.TABLE_NAME, null, values);

                if (_id > 0)
                    returnUri = MoviesEntry.buildMoviesUri(_id);
                else
                    throw new android.database.SQLException("Failed to insert row into " + uri);

                break;
            }
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);

        return returnUri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        //Start by getting a writable database
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        //Use the uriMatcher to match the MOVIES URI's we are going to handle.
        final int match = sUriMatcher.match(uri);

        int rowsDeleted;
        //A null value deletes all rows.  In my implementation of this, I only notified
        // the uri listeners (using the content resolver) if the rowsDeleted != 0 or the selection
        // is null.
        switch (match) {
            case MOVIES:
                rowsDeleted = db.delete(MoviesEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case MOVIE_ID:
                String id = uri.getLastPathSegment();
                if (TextUtils.isEmpty(selection)) {
                    rowsDeleted = db.delete(MoviesEntry.TABLE_NAME,
                            MoviesEntry._ID + "=" + id,
                            null);
                } else {
                    rowsDeleted = db.delete(MoviesEntry.TABLE_NAME,
                            MoviesEntry._ID + "=" + id + " and " + selection, selectionArgs);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        Log.d(LOG_TAG, "Deleted rows: " + rowsDeleted);
        Log.d(LOG_TAG, "Uri: " + uri.toString());

        if (rowsDeleted != 0) getContext().getContentResolver().notifyChange(uri, null);

        return rowsDeleted;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        //This is a lot like the delete function.  We return the number of rows impacted
        // by the update.
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();

        final int match = sUriMatcher.match(uri);
        int rowsUpdated;

        switch (match) {
            case MOVIES:
                rowsUpdated = db.update(MoviesEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case MOVIE_ID:
                String id = uri.getLastPathSegment();
                if (TextUtils.isEmpty(selection)) {
                    rowsUpdated = db.update(MoviesEntry.TABLE_NAME,
                            values,
                            MoviesEntry._ID + "=" + id,
                            null);
                } else {
                    rowsUpdated = db.update(MoviesEntry.TABLE_NAME,
                            values,
                            MoviesEntry._ID + "=" + id
                            + " and "
                            + selection,
                            selectionArgs);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        if (rowsUpdated != 0) getContext().getContentResolver().notifyChange(uri, null);

        return rowsUpdated;
    }

    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case MOVIES:
                db.beginTransaction();
                int returnCount = 0;
                try {
                    for (ContentValues value: values) {
                        long _id = db.insert(MoviesEntry.TABLE_NAME, null, value);
                        if (_id != -1) {
                            returnCount++;
                        }
                    }
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
                getContext().getContentResolver().notifyChange(uri, null);
                return returnCount;
            default:
                return super.bulkInsert(uri, values);
        }
    }

    // You do not need to call this method. This is a method specifically to assist the testing
    // framework in running smoothly. You can read more at:
    // http://developer.android.com/reference/android/content/ContentProvider.html#shutdown()
    @Override
    @TargetApi(11)
    public void shutdown() {
        mOpenHelper.close();
        super.shutdown();
    }
}

This is a snippet, where I'm updating my data:

for (Movie movie: moviesList) {
    ContentValues movieValues = new ContentValues();

    timeCounter++;

    movieValues.put(MoviesEntry.COLUMN_TITLE, movie.getTitle());
    movieValues.put(MoviesEntry.COLUMN_DIRECTORS, movie.getDirectors().get(0).getName());
    movieValues.put(MoviesEntry.COLUMN_GENRES, movie.getGenres().toString());
    movieValues.put(MoviesEntry.COLUMN_WRITERS, movie.getWriters().get(0).getName());
    movieValues.put(MoviesEntry.COLUMN_COUNTRIES, movie.getCountries().get(0));
    movieValues.put(MoviesEntry.COLUMN_YEAR, movie.getYear());
    movieValues.put(MoviesEntry.COLUMN_RUNTIME, movie.getRuntime().get(0));
    movieValues.put(MoviesEntry.COLUMN_URL_POSTER, movie.getUrlPoster());
    movieValues.put(MoviesEntry.COLUMN_RATING, movie.getRating());
    movieValues.put(MoviesEntry.COLUMN_PLOT, movie.getPlot());
    movieValues.put(MoviesEntry.COLUMN_URL_IMDB, movie.getUrlIMDB());
    movieValues.put(MoviesEntry.COLUMN_DATE, dateTime + timeCounter);

    Log.d(LOG_TAG, "" + dateTime);

    downloadPosters(movie.getUrlPoster());

    cVVector.add(movieValues);
}

//add to database
if (cVVector.size() > 0) {
    ContentValues[] cvArray = new ContentValues[cVVector.size()];
    cVVector.toArray(cvArray);
    int inserted = getContext().getContentResolver()
            .bulkInsert(MoviesEntry.CONTENT_URI, cvArray);
    Log.d(LOG_TAG, "Inserted: " + inserted);
    Log.d(LOG_TAG, "DateTime: " + dateTime);

    Cursor cursor = getContext().getContentResolver().query(MoviesEntry.CONTENT_URI,
            null, null, null, null);
    if (cursor.getCount() > numberOfMovies) {
        // delete old data so we don't build up an endless history
        int deleted = getContext().getContentResolver().delete(MoviesEntry.CONTENT_URI,
                MoviesEntry.COLUMN_DATE + "<?",
                new String[] {Long.toString(dateTime)});
        Log.d(LOG_TAG, "Deleted: " + deleted);
        Log.d(LOG_TAG, "DateTime#2: " + dateTime);
    }

    updateNotifications();
}

This is my snippet from the main fragment, where I implemented RecycleView and Loaders (it works fine):

public class MoviesFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final String LOG_TAG = MoviesFragment.class.getSimpleName();

    private MoviesAdapter mMoviesAdapter;
    private RecyclerView mRecyclerView;
    private RecyclerView.LayoutManager mLayoutManager;

    private static final String[] CARDS_PROJECTION = {
            MoviesEntry._ID,
            MoviesEntry.COLUMN_URL_POSTER,
            MoviesEntry.COLUMN_TITLE,
            MoviesEntry.COLUMN_DIRECTORS,
            MoviesEntry.COLUMN_GENRES,
            MoviesEntry.COLUMN_RATING,
            MoviesEntry.COLUMN_YEAR
    };

    private static final int CARDS_LOADER = 0;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.movies_list, container, false);

        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.list_of_movies);
        mLayoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(mLayoutManager);

        mMoviesAdapter = new MoviesAdapter(getActivity(), null, 0);
        mRecyclerView.setAdapter(mMoviesAdapter);

        mRecyclerView.addOnItemTouchListener(
                new RecyclerItemClickListener(getActivity(), new RecyclerItemClickListener.OnItemClickListener() {
                    @Override
                    public void onItemClick(View view, int position) {

                        Log.d(LOG_TAG, "POSITION: " + position);
                        //Because position starts from 0
                        Cursor cursor = mMoviesAdapter.getItem(position + 1);

                        Uri uri = MoviesEntry.buildMoviesUri((long) cursor.getPosition());
                        Intent intent = new Intent(getActivity(), DetailActivity.class);
                        intent.putExtra(Utility.ID_KEY, uri.toString());
                        startActivity(intent);
                    }
                })
        );

        return rootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        getLoaderManager().initLoader(CARDS_LOADER, null, this);
        super.onActivityCreated(savedInstanceState);
    }


    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri baseUri = MoviesEntry.CONTENT_URI;

        return new CursorLoader(getActivity(), baseUri, CARDS_PROJECTION, null, null, null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        mMoviesAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mMoviesAdapter.swapCursor(null);
    }
}

And finally my detail fragment class, where I'm updating data using Loaders:

public class DetailFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final String LOG_TAG = DetailFragment.class.getSimpleName();
    private static final String DETAIL_URI = "URI";
    private Uri mUri;

    private static final int DETAIL_LOADER = 0;

    private static final String[] DETAIL_COLUMNS = {
            MoviesEntry.TABLE_NAME + "." + MoviesEntry._ID,
            MoviesEntry.COLUMN_URL_POSTER,
            MoviesEntry.COLUMN_TITLE,
            MoviesEntry.COLUMN_COUNTRIES,
            MoviesEntry.COLUMN_YEAR,
            MoviesEntry.COLUMN_RUNTIME,
            MoviesEntry.COLUMN_RATING,
            MoviesEntry.COLUMN_GENRES,
            MoviesEntry.COLUMN_DIRECTORS,
            MoviesEntry.COLUMN_WRITERS,
            MoviesEntry.COLUMN_PLOT,
            MoviesEntry.COLUMN_URL_IMDB
    };


    private static final String SHARE_HASHTAG = " #WhatToWatch";
    private ShareActionProvider mShareActionProvider;
    private String mMovieShareInfo;

    private ImageView mPosterView;
    private TextView mTitleView;
    private TextView mCountriesView;
    private TextView mYearView;
    private TextView mRuntimeView;
    private TextView mRatingView;
    private TextView mGenresView;
    private TextView mDirectorsView;
    private TextView mWritersView;
    private TextView mPlotView;

    public DetailFragment() {
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_detail, container, false);


        Bundle extras = getActivity().getIntent().getExtras();
        if (extras != null) {
            mUri = Uri.parse(extras.getString(Utility.ID_KEY));
        }

        mPosterView = (ImageView) rootView.findViewById(R.id.poster);
        mTitleView = (TextView) rootView.findViewById(R.id.tv_title);
        mCountriesView = (TextView) rootView.findViewById(R.id.country);
        mYearView = (TextView) rootView.findViewById(R.id.release_year);
        mRuntimeView = (TextView) rootView.findViewById(R.id.runtime);
        mRatingView = (TextView) rootView.findViewById(R.id.rating);
        mGenresView = (TextView) rootView.findViewById(R.id.genre);
        mDirectorsView = (TextView) rootView.findViewById(R.id.director);
        mWritersView = (TextView) rootView.findViewById(R.id.writers);
        mPlotView = (TextView) rootView.findViewById(R.id.plot);

        return rootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        getLoaderManager().initLoader(DETAIL_LOADER, null, this);
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        // Inflate the menu; this adds items to the action bar if it is present.
        inflater.inflate(R.menu.detailfragment, menu);
        // Retrieve the share menu item
        MenuItem menuItem = menu.findItem(R.id.action_share);
        // Get the provider and hold onto it to set/change the share intent.
        mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem);

        if (mMovieShareInfo == null) {
            mShareActionProvider.setShareIntent(createShareMovieIntent());
        }
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {

        if (null != mUri) {
            return new CursorLoader(
                    getActivity(),
                    mUri,
                    DETAIL_COLUMNS,
                    null,
                    null,
                    null
            );
        }

        return null;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

        updateData(data);

    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Log.d(LOG_TAG, "Data reseted!");
    }

    private Intent createShareMovieIntent() {
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
        shareIntent.setType("text/plain");
        shareIntent.putExtra(Intent.EXTRA_TEXT, mMovieShareInfo + SHARE_HASHTAG);

        return shareIntent;
    }

    private void updateData(Cursor data) {
        if (data != null && data.moveToFirst()) {
            String posterUrl = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_URL_POSTER));
            String title = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_TITLE));
            String country = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_COUNTRIES));
            String year = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_YEAR));
            String runtime = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_RUNTIME));
            String rating = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_RATING));
            String genres = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_GENRES));
            String director = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_DIRECTORS));
            String writer = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_WRITERS));
            String plot = data.getString(data.getColumnIndex(MoviesEntry.COLUMN_PLOT));

            Picasso.with(getActivity())
                    .load(posterUrl)
                    .placeholder(R.drawable.progress_animation)
                    .resize(205, 310)
                    .centerCrop()
                    .into(mPosterView);
            mTitleView.setText(title);
            mCountriesView.setText(country);
            mYearView.setText(year);
            mRuntimeView.setText(runtime);
            mRatingView.setText(rating);
            mGenresView.setText(genres);
            mDirectorsView.setText(director);
            mWritersView.setText(writer);
            mPlotView.setText(plot);

            mMovieShareInfo = "Awesome movie «" + title + "»" + "\n" +
                    "which IMDB rating is " + rating + "\n" +
                    "And directed by " + director + "\n" + genres + "\n";

            if (mShareActionProvider != null) {
                mShareActionProvider.setShareIntent(createShareMovieIntent());
            }
        }
    }
}

After first start of the application (when only bulkInsert() method calls), everythings works fine and loader load the data correctly. But when I'm updating data (when bulkInsert(), then query() and delete() (which delete old data) methods calls) CursorLoader doesn't load the new data to the DetailFragment and it looks like this:

enter image description here


Solution

  • Because in SQLite autoincremented id counter does not resets, so I just replaced this:

    Cursor cursor = mMoviesAdapter.getItem(position + 1);
    Uri uri = MoviesEntry.buildMoviesUri((long) cursor.getPosition());
    

    by this:

    Uri uri = MoviesEntry.buildMoviesUri(mMoviesAdapter.getItemId(position));
    

    in my main fragment. And everything works fine.