Android: Refresh ListFragement using ContentProvider and Loader - android

I am using a SQLite database and a ContentProvider to fill a ListFragment. The problem is that ListFragment is not getting refreshed after I add a item. The ListFragment is empty. I have to close and reopen the app to show the added item in the list.
I try to update it like this:
public class RoomListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
//adapter using SQLite and ContentProvider to fill ListFragment
private SimpleCursorAdapter dataAdapter;
//needed for create room dialog
private EditText enter_room;
private static View textEntryView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//display ActionBar items
setHasOptionsMenu(true);
// TODO: replace with a real list adapter.
displayListView();
}
#Override
public void onResume() {
super.onResume();
//Starts a new or restarts an existing Loader in this manager
getLoaderManager().restartLoader(0, null, this);
}
#Override
public void onDestroy() {
super.onDestroy();
}
private void displayListView() {
// The desired columns to be bound
String[] columns = new String[] {
Database.KEY_GROUPADDRESS,
Database.KEY_NAME,
Database.KEY_DPT
};
// the XML defined views which the data will be bound to
int[] to = new int[] {
R.id.groupaddress,
R.id.name,
R.id.dpt,
};
// create an adapter from the SimpleCursorAdapter
dataAdapter = new SimpleCursorAdapter(
getActivity(),
R.layout.device_info,
null,
columns,
to,
0);
//set SimpleCursorAdapter to ListFragmentAdapter
setListAdapter(dataAdapter);
//Ensures a loader is initialized and active.
getLoaderManager().initLoader(0, null, this);
}
// This is called when a new Loader needs to be created.
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String[] projection = {
Database.KEY_ROWID,
Database.KEY_GROUPADDRESS,
Database.KEY_NAME,
Database.KEY_DPT};
CursorLoader cursorLoader = new CursorLoader(getActivity(),
MyContentProvider.CONTENT_URI, projection, null, null, null);
return cursorLoader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
dataAdapter.swapCursor(data);
dataAdapter.notifyDataSetChanged();
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
dataAdapter.swapCursor(null);
}
I add a item with this code:
//Handle OnClick events on ActionBar items
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// handle item selection
switch (item.getItemId()) {
case R.id.menu_add:
//Toast.makeText(getActivity(), "Click", Toast.LENGTH_SHORT).show();
LayoutInflater factory = LayoutInflater.from(getActivity());
//textEntryView is an Layout XML file containing text field to display in alert dialog
textEntryView = factory.inflate(R.layout.dialog_add_room, null);
//get the control from the layout
enter_room = (EditText) textEntryView.findViewById(R.id.enter_room);
//create Dialog
final AlertDialog.Builder alert1 = new AlertDialog.Builder(getActivity());
//configure dialog
alert1.setTitle("Raum hinzufügen:").setView(textEntryView)
.setPositiveButton("Hinzufügen",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
String roomname = enter_room.getText().toString();
Log.d("Insert: ", "Inserting ..");
ContentValues values = new ContentValues();
//TODO Richtige Spalte für Raumname verwenden
values.put(Database.KEY_NAME, roomname);
getActivity().getContentResolver().insert(MyContentProvider.CONTENT_URI, values);
dataAdapter.notifyDataSetChanged();
}
}).setNegativeButton("Abbrechen",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
//cancel dialog
}
});
alert1.show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
My ContentProvider:
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class MyContentProvider extends ContentProvider{
private MyDatabaseHelper dbHelper;
private static final int ALL_COUNTRIES = 1;
private static final int SINGLE_COUNTRY = 2;
// authority is the symbolic name of your provider
// To avoid conflicts with other providers, you should use
// Internet domain ownership (in reverse) as the basis of your provider authority.
private static final String AUTHORITY = "de.mokkapps.fixknxdemo.contentprovider";
// create content URIs from the authority by appending path to database table
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/countries");
// a content URI pattern matches content URIs using wildcard characters:
// *: Matches a string of any valid characters of any length.
// #: Matches a string of numeric characters of any length.
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "countries", ALL_COUNTRIES);
uriMatcher.addURI(AUTHORITY, "countries/#", SINGLE_COUNTRY);
}
// system calls onCreate() when it starts up the provider.
#Override
public boolean onCreate() {
// get access to the database helper
dbHelper = new MyDatabaseHelper(getContext());
return false;
}
//Return the MIME type corresponding to a content URI
#Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
return "vnd.android.cursor.dir/vnd.com.as400samplecode.contentprovider.countries";
case SINGLE_COUNTRY:
return "vnd.android.cursor.item/vnd.com.as400samplecode.contentprovider.countries";
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
// The insert() method adds a new row to the appropriate table, using the values
// in the ContentValues argument. If a column name is not in the ContentValues argument,
// you may want to provide a default value for it either in your provider code or in
// your database schema.
#Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
long id = db.insert(Database.SQLITE_TABLE, null, values);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(CONTENT_URI + "/" + id);
}
// The query() method must return a Cursor object, or if it fails,
// throw an Exception. If you are using an SQLite database as your data storage,
// you can simply return the Cursor returned by one of the query() methods of the
// SQLiteDatabase class. If the query does not match any rows, you should return a
// Cursor instance whose getCount() method returns 0. You should return null only
// if an internal error occurred during the query process.
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Database.SQLITE_TABLE);
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
queryBuilder.appendWhere(Database.KEY_ROWID + "=" + id);
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
return cursor;
}
// The delete() method deletes rows based on the selection or if an id is
// provided then it deleted a single row. The methods returns the numbers
// of records delete from the database. If you choose not to delete the data
// physically then just update a flag here.
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
selection = Database.KEY_ROWID + "=" + id
+ (!TextUtils.isEmpty(selection) ?
" AND (" + selection + ')' : "");
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int deleteCount = db.delete(Database.SQLITE_TABLE, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return deleteCount;
}
// The update method() is same as delete() which updates multiple rows
// based on the selection or a single row if the row id is provided. The
// update method returns the number of updated rows.
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
selection = Database.KEY_ROWID + "=" + id
+ (!TextUtils.isEmpty(selection) ?
" AND (" + selection + ')' : "");
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int updateCount = db.update(Database.SQLITE_TABLE, values, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
}

You should call notifyDataSetChanged(); from within your content providers' insert method and supply the URI as an argument.
At the point in time you are currently calling the notifyDataSetChanged(); method the insert may not have actually happened as the content provider call to insert will be handled asynchronously.
An example could look something like this
#Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
int uriType = sURIMatcher.match(uri);
long id;
switch (uriType) {
case TEAMS:
id = sqlDB.replace(TeamModel.TEAM_TABLE_NAME, null, values);
break;
case CARS:
id = sqlDB.replace(CarModel.CAR_TABLE_NAME, null, values);
break;
case TEAM_ERRORS:
id = sqlDB.replace(TeamErrorModel.TEAMS_ERRORS_TABLE_NAME, null, values);
String teamId = values.get(TeamErrorModel.COL_TEAM_ID).toString();
String selection = TeamModel.COL_ID + " = ?";
String[] selectionArgs = {teamId};
setErrorFlagTeamModel(sqlDB, true, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null, false);
return Uri.parse(uri + "/" + id);
}
The first argument is the URI passed in to the insert method and will tell ALL adapters listening in on that particular uri to update their data.
The last argument (false) tells a sync adapter to ignore this change. I assume you are not using a sync adapter
All methods in your ContentProvider should call the notifyChange method in a similar way.
You may well find that the insert actually failed. so check that the records are actually being inserted.
UPDATE
As per comment below from #zapi
And you need to add cursor.setNotificationUri(contentresolver, uri)
inside the query method or the Cursor does not know for which uri
notification it has to listen
Since answering your question you have posted your content provider and I can now see that in fact as per the above quote this is in fact your missing link

Related

Fixing a SQL Injection Vulnerability on Content Provider Android

I have different apps that share some data with each others, that's done through Content Provider but when I uploaded the apk I received an email saying "Your app(s) are using a content provider that contains a SQL Injection vulnerability."
There are a couple of ways for fixing this according to Google guide:
If an affected ContentProvider needs to be exposed to other apps:
You can prevent SQL Injection into SQLiteDatabase.query by using
strict mode with a projection map. Strict mode protects against
malicious selection clauses and projection map protects against
malicious projection clauses. You must use both of these features to ensure that your queries are safe.
You can prevent SQL Injection into SQLiteDatabase.update and SQLiteDatabase.delete by using a selection clause that uses '?' as a replaceable parameter and a separate array of selection arguments. Your selection clause should not be constructed from untrusted inputs.
But is not clear to me how to proceed with any of the solutions, I don't get exactly how to use the projection map or change the code using selection clause that uses '?'. I mean, I have seen some examples about ProjectionMap but what are the key/value it needs for the query? Do I need to write explicit the values I want? but what if it is a generic method and I don't know in that part of the code what do I want to get? Or how do I convert any query to a ProjectionMap?
I hope I'm explaining myself on this.
Here is my code:
#Override
public boolean onCreate() {
gOpenHelper = new GameDBHelper(getContext());
return true;
}
/**
* Builds a UriMatcher that is used to determine witch database request is being made.
*/
public static UriMatcher buildUriMatcher(){
String content = GamesContract.CONTENT_AUTHORITY;
// All paths to the UriMatcher have a corresponding code to return
// when a match is found (the ints above).
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(content, GamesContract.PATH_GAME, GAME);
matcher.addURI(content, GamesContract.PATH_GAME + "/#", GAME_ID);
return matcher;
}
#Override
public String getType(Uri uri) {
switch(sUriMatcher.match(uri))
{
case GAME:
return GameEntry.CONTENT_TYPE;
case GAME_ID:
return GameEntry.CONTENT_ITEM_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
Cursor retCursor;
switch(sUriMatcher.match(uri))
{
case GAME:
retCursor = db.query(
GameEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
break;
case GAME_ID:
long _id = ContentUris.parseId(uri);
retCursor = db.query(
GameEntry.TABLE_NAME,
projection,
GameEntry._ID + " = ?",
new String[]{String.valueOf(_id)},
null,
null,
sortOrder
);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
retCursor.setNotificationUri(getContext().getContentResolver(), uri);
return retCursor;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
long _id;
Uri returnUri;
switch(sUriMatcher.match(uri))
{
case GAME:
_id = db.insert(GameEntry.TABLE_NAME, null, values);
if(_id > 0){
returnUri = GameEntry.BuildGameUri(_id);
} else{
throw new UnsupportedOperationException("Unable to insert rows 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) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
int rows; // Number of rows effected
switch(sUriMatcher.match(uri))
{
case GAME:
rows = db.delete(GameEntry.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// Because null could delete all rows:
if(selection == null || rows != 0){
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = gOpenHelper.getWritableDatabase();
int rows;
switch(sUriMatcher.match(uri))
{
case GAME:
rows = db.update(GameEntry.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if(rows != 0){
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
}
To anyone who can be looking for the answer, it was pretty simpe, I think it was getting hard for me because I wasn't familiar with many concepts about sql on Android.
The problem goes with the queries that require a 'where' clause; in my case update and delete, the things I needed to do in order to fix it was to not use the selection parameter as it is, I needed to do something like:
GamesContract.Games.GAME_NAME + " = ?"
db.delete(GamesContract.Games.TABLE_NAME, GamesContract.Games.GAME_NAME + " = ?", selectionArgs);
Basicaly the table name + " = ?" selection.
This way you are forcing to update only the needed table and prevent the sql injection to modify any place in your database through that same method.
I fixed it using SQLiteQueryBuilder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(TABLE_NAME);
builder.setProjectionMap(buildProjectionMap());
builder.setStrict(true);
count = builder.delete(db, selection, selectionArgs);
}else {
count = db.delete(TABLE_NAME, selection, selectionArgs);
}

Deleting data from SQLite database doesn't work in Fragment

I'm having an Activity which handles a details Fragment for my Movie data & I want to implement favorite functionality. But the issue is that I can favorite only one movie at a time. Also, each time I try to add/favorite a movie, it'll show on the database that it's being saved but never being deleted on unfavorite.
Here's the code that I've:
MovieDetailsActivity
public class MovieDetailsActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* Set the content of the activity to use the activity_tv_show_details.xml layout file */
setContentView(R.layout.activity_movie_details);
Bundle movieDetails = new Bundle();
/**get the movie's Object from the parent activity**/
Movie movie = getIntent().getParcelableExtra("movie");
movieDetails.putParcelable("movie", movie);
Intent intent = getIntent();
Uri mCurrentMovieUri = intent.getData();
movieDetails.putString("currentMovieUri", mCurrentMovieUri.toString());
/* Check for pre-existing instances of fragments(here explicitly check for savedInstance)
and then begin fragment transaction accordingly */
if (savedInstanceState == null) {
MovieDetailsFragment defaultMovieFragment = new MovieDetailsFragment();
defaultMovieFragment.setArguments(movieDetails);
getSupportFragmentManager().beginTransaction()
.add(R.id.containerMovieDetailActivity, defaultMovieFragment)
.commit();
}
}
}
MovieDetailsFragment
public class MovieDetailsFragment extends Fragment implements LoaderManager.LoaderCallbacks<MovieDetailsBundle> {
private static final int MOVIE_DETAIL_LOADER_ID = 2;
/* Arrays for holding movie details */
Movie movie;
public MovieDetailsFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_movie_detail, container, false);
Bundle bundle = getArguments();
position = bundle.getInt("position");
currentMovieUri = Uri.parse(bundle.getString("currentMovieUri"));
favoriteButton = (ImageButton) rootView.findViewById(R.id.favorite);
if (savedInstanceState == null) {
mReview = new ArrayList<>();
mVideo = new ArrayList<>();
mCredits = new ArrayList<>();
mMovieDetailsBundle = new MovieDetailsBundle();
}
if ((bundle != null)) {
movie = getArguments().getParcelable("movie");
movieDetailTitleTextView.setText(movie.getMovieTitle());
...
String[] projection = {
MoviesEntry._ID,
MoviesEntry.COLUMN_MOVIE_TITLE,
MoviesEntry.COLUMN_MOVIE_RELEASE_DATE,
MoviesEntry.COLUMN_MOVIE_OVERVIEW,
MoviesEntry.COLUMN_MOVIE_POSTER_URL,
MoviesEntry.COLUMN_MOVIE_BACKDROP_URL,
MoviesEntry.COLUMN_MOVIE_RATING};
// Perform a query on the provider using the ContentResolver.
// Use the {#link MoviesEntry#CONTENT_URI} to access the pet data.
Cursor cursor = getActivity().getContentResolver().query(
MoviesEntry.CONTENT_URI, // The content URI of the movies table
projection, // The columns to return for each row
null, // Selection criteria
null, // Selection criteria
null);
try {
// Figure out the index of each column
int idColumnIndex = cursor.getColumnIndex(MoviesEntry._ID);
int titleColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_TITLE);
int releaseDateColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_RELEASE_DATE);
int overviewColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_OVERVIEW);
int posterUrlColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_POSTER_URL);
int backdropUrlColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_BACKDROP_URL);
int ratingColumnIndex = cursor.getColumnIndex(MoviesEntry.COLUMN_MOVIE_RATING);
// Iterate through all the returned rows in the cursor
if (cursor.moveToFirst()){
// Use that index to extract the String or Int value of the word
// at the current row the cursor is on.
currentID = cursor.getInt(idColumnIndex);
currentTitle = cursor.getString(titleColumnIndex);
currentReleaseDate = cursor.getString(releaseDateColumnIndex);
currentOverview = cursor.getString(overviewColumnIndex);
currentposterUrl = cursor.getString(posterUrlColumnIndex);
currentBackdropUrl = cursor.getString(backdropUrlColumnIndex);
currentRatings = cursor.getFloat(ratingColumnIndex);
}
} finally {
// Always close the cursor when you're done reading from it. This releases all its
// resources and makes it invalid.
cursor.close();
}
if (currentTitle!=null) {
//currentTitle = movie.getMovieTitle();
if (currentTitle.equals(movie.getMovieTitle())) {
favoriteButton.setImageResource(R.drawable.starred);
favorite = true;
}
}else{
favoriteButton.setImageResource(R.drawable.unstarred);
favorite = false;
}
/*setting the ratingbar from #link: https://github.com/FlyingPumba/SimpleRatingBar*/
SimpleRatingBar simpleRatingBar = (SimpleRatingBar) rootView.findViewById(R.id.movieRatingInsideMovieDetailsFragment);
simpleRatingBar.setRating((float) (movie.getMovieVoteAverage()) / 2);
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (favorite) {
// Only perform the delete if this is an existing movie.
if (currentMovieUri != null) {
// Call the ContentResolver to delete the movie at the given content URI.
// Pass in null for the selection and selection args because the mCurrentPetUri
// content URI already identifies the movie that we want.
int rowsDeleted = getActivity().getContentResolver().delete(currentMovieUri, null, null);
// Show a toast message depending on whether or not the delete was successful.
if (rowsDeleted == 0) {
// If no rows were deleted, then there was an error with the delete.
Toast.makeText(getContext(), getString(R.string.delete_movie_failed),
Toast.LENGTH_SHORT).show();
} else {
// Otherwise, the delete was successful and we can display a toast.
Toast.makeText(getContext(), getString(R.string.delete_movie_successful),
Toast.LENGTH_SHORT).show();
}
}
favoriteButton.setImageResource(R.drawable.unstarred);
} else {
// Create a ContentValues object where column names are the keys,
// and movie attributes from the editor are the values.
ContentValues values = new ContentValues();
values.put(MoviesEntry.COLUMN_MOVIE_TITLE, movie.getMovieTitle());
values.put(MoviesEntry.COLUMN_MOVIE_RELEASE_DATE, movie.getMovieReleaseDate());
values.put(MoviesEntry.COLUMN_MOVIE_OVERVIEW, movie.getMovieOverview());
values.put(MoviesEntry.COLUMN_MOVIE_POSTER_URL, movie.getMoviePosterPath());
values.put(MoviesEntry.COLUMN_MOVIE_BACKDROP_URL, movie.getMovieBackdropPath());
values.put(MoviesEntry.COLUMN_MOVIE_RATING, movie.getMovieVoteAverage() / 2);
// Insert a new movie into the provider, returning the content URI for the new movie.
Uri newUri = getActivity().getContentResolver().insert(MoviesEntry.CONTENT_URI, values);
// Show a toast message depending on whether or not the insertion was successful
if (newUri == null) {
// If the new content URI is null, then there was an error with insertion.
Toast.makeText(getContext(), getString(R.string.insert_movie_failed),
Toast.LENGTH_SHORT).show();
} else {
// Otherwise, the insertion was successful and we can display a toast.
Toast.makeText(getContext(), getString(R.string.insert_movie_successful),
Toast.LENGTH_SHORT).show();
}
favoriteButton.setImageResource(R.drawable.starred);
}
favorite = !favorite;
}
});
...
#Override
public Loader<MovieDetailsBundle> onCreateLoader(int id, Bundle args) {
Uri baseUri = Uri.parse((UrlsAndConstants.MovieDetailQuery.DEFAULT_URL) + movie.getMovieId());
Uri.Builder uriBuilder = baseUri.buildUpon();
uriBuilder.appendQueryParameter(API_KEY_PARAM, API_KEY_PARAM_VALUE);
uriBuilder.appendQueryParameter(APPEND_TO_RESPONSE, VIDEOS_AND_REVIEWS_AND_CREDITS);
return new DetailsMovieLoader(getActivity().getApplicationContext(), uriBuilder.toString());
}
#Override
public void onLoadFinished(Loader<MovieDetailsBundle> loader, )
...
}
public void updateDurationTextView(MovieDetailsBundle movieDetailsBundle) {
...
movieRunTimeDuration.setText(mMovieDurationString);
}
#Override
public void onLoaderReset(Loader<MovieDetailsBundle> loader) {
}
}
ContentProvider
public class MovieProvider extends ContentProvider {
/**
* URI matcher code for the content URI for the movies table
*/
private static final int MOVIES = 100;
/**
* URI matcher code for the content URI for a single movie in the movie table
*/
private static final int MOVIE_ID = 200;
/**
* 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 static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Static initializer. This is run the first time anything is called from this class.
static {
// 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.movie/movie" will map to the
// integer code {#link #MOVIES}. This URI is used to provide access to MULTIPLE rows
// of the movie table.
sUriMatcher.addURI(MovieContract.CONTENT_AUTHORITY, MovieContract.PATH_MOVIES, MOVIES);
// The content URI of the form "content://com.example.android.movie/movie/#" will map to the
// integer code {#link #MOVIES}. This URI is used to provide access to ONE single row
// of the movie table.
//
// In this case, the "#" wildcard is used where "#" can be substituted for an integer.
// For example, "content://com.example.android.movie/movie/3" matches, but
// "content://com.example.android.movie/movie" (without a number at the end) doesn't match.
sUriMatcher.addURI(MovieContract.CONTENT_AUTHORITY, MovieContract.PATH_MOVIES + "/#", MOVIE_ID);
}
/**
* Database helper object
*/
private MovieDbHelper mDbHelper;
#Override
public boolean onCreate() {
mDbHelper = new MovieDbHelper(getContext());
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Get readable database
SQLiteDatabase database = mDbHelper.getReadableDatabase();
// This cursor will hold the result of the query
Cursor cursor;
// Figure out if the URI matcher can match the URI to a specific code
int match = sUriMatcher.match(uri);
switch (match) {
case MOVIES:
// For the MOVIES code, query the movie table directly with the given
// projection, selection, selection arguments, and sort order. The cursor
// could contain multiple rows of the movie table.
cursor = database.query(MoviesEntry.TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case MOVIE_ID:
// For the MOVIE_ID code, extract out the ID from the URI.
// For an example URI such as "content://com.example.android.movie/movie/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.
selection = MoviesEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
// This will perform a query on the movie table where the _id equals 3 to return a
// Cursor containing that row of the table.
cursor = database.query(MoviesEntry.TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
default:
throw new 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(getContext().getContentResolver(), uri);
// Return the cursor
return cursor;
}
#Override
public Uri insert(Uri uri, ContentValues contentValues) {
final int match = sUriMatcher.match(uri);
switch (match) {
case MOVIES:
return insertMovie(uri, contentValues);
default:
throw new IllegalArgumentException("Insertion is not supported for " + uri);
}
}
/**
* Insert a movie into the database with the given content values. Return the new content URI
* for that specific row in the database.
*/
private Uri insertMovie(Uri uri, ContentValues values) {
// Log.v("my_tag", "Received Uri to be matched insert is :"+uri.toString());
Log.i("my_tag", "Received Uri to be matched insert is :"+uri.toString());
// Check that the product name is not null
String title = values.getAsString(MoviesEntry.COLUMN_MOVIE_TITLE);
if (title == null) {
throw new IllegalArgumentException("Product requires a name");
}
// Check that the product name is not null
String releaseDate = values.getAsString(MoviesEntry.COLUMN_MOVIE_RELEASE_DATE);
if (releaseDate == null) {
throw new IllegalArgumentException("Product requires a detail");
}
// If the price is provided, check that it's greater than or equal to 0
String overview = values.getAsString(MoviesEntry.COLUMN_MOVIE_OVERVIEW);
if (overview == null) {
throw new IllegalArgumentException("Product requires valid price");
}
String posterUrl = values.getAsString(MoviesEntry.COLUMN_MOVIE_POSTER_URL);
if (posterUrl == null) {
throw new IllegalArgumentException("Product requires valid quantity");
}
String backdropUrl = values.getAsString(MoviesEntry.COLUMN_MOVIE_BACKDROP_URL);
if (backdropUrl == null) {
throw new IllegalArgumentException("Product requires valid quantity");
}
Integer ratings = values.getAsInteger(MoviesEntry.COLUMN_MOVIE_RATING);
if (ratings != null && ratings < 0) {
throw new IllegalArgumentException("Product requires valid quantity");
}
// Get writeable database
SQLiteDatabase database = mDbHelper.getWritableDatabase();
// Insert the new product with the given values
long id = database.insert(MoviesEntry.TABLE_NAME, null, values);
Log.i("my_tag", "values is :"+values.toString());
// Log.e("my_tag", "values is :"+values.toString());
// Log.v("my_tag", "values is :"+values.toString());
if (id == -1) {
return null;
}
// Notify all listeners that the data has changed for the product content URI
getContext().getContentResolver().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
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Get writeable database
SQLiteDatabase database = mDbHelper.getWritableDatabase();
// Track the number of rows that were deleted
int rowsDeleted;
final int match = sUriMatcher.match(uri);
case MOVIES:
// Delete all rows that match the selection and selection args
rowsDeleted = database.delete(MoviesEntry.TABLE_NAME, selection, selectionArgs);
break;
case MOVIE_ID:
// Delete a single row given by the ID in the URI
selection = MoviesEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
Log.i("my_tag", "selection is :"+selection);
// Log.e("my_tag", "selection is :"+selection);
// Log.v("my_tag", "selection is :"+selection);
Log.i("my_tag", "selectionArgs is :"+selectionArgs[0]);
// Log.e("my_tag", "selectionArgs is :"+selectionArgs[0]);
// Log.v("my_tag", "selectionArgs is :"+selectionArgs[0]);
rowsDeleted = database.delete(MoviesEntry.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new 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) {
getContext().getContentResolver().notifyChange(uri, null);
}
// Return the number of rows deleted
return rowsDeleted;
}
#Override
public int update(Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
...
}
#Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case MOVIES:
return MoviesEntry.CONTENT_LIST_TYPE;
case MOVIE_ID:
return MoviesEntry.CONTENT_ITEM_TYPE;
default:
throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
}
}
}

Loader returns no data, or an empty cursor

I've got two loaders, each of which loads data from a different content provider.
The fragment is supplied with the ID of a medication from the first content provider, and the first loader loads all of the information related to that medication. The second loader is supposed to query the second content provider for all of the alarms associated with that medication.
The first loader works just fine, and returns all of the correct data. However, the second loader appears to return a null cursor, even though I know for a fact that there is plenty of data in the table that should be relevant. I say "appears" because using getCount() on the data in in onLoadFinished for the second loader causes my app to crash, and the only reason I can think that this would occur is if the cursor were null.
Anyway, here's the code for my loaders. If you need, I can give you the code for anything else you want.
/**
* Initializes the loaders.
*/
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
CursorLoader loader = null;
long id = getArguments().getLong(ARG_MED_ID);
switch(loaderId) {
case 0: // MedList Loader
Log.d("MedManager", "Loading med data");
Uri singleUri = ContentUris.withAppendedId(MedProvider.CONTENT_URI, id);
String[] projection = { MedTable.MED_ID,
MedTable.MED_NAME,
MedTable.MED_DOSAGE,
MedTable.MED_DATE_FILLED,
MedTable.MED_DURATION };
loader = new CursorLoader(getActivity(), singleUri,
projection, null, null,
MedTable.MED_NAME + " COLLATE LOCALIZED ASC");
break;
case 1: // AlarmList Loader
Log.d("MedManager", "Theoretically loading alarm list");
Uri baseUri = AlarmProvider.CONTENT_URI;
// Create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String[] alarmProjection = { DailyAlarmTable.ALARM_ID,
DailyAlarmTable.ALARM_MEDNUM,
DailyAlarmTable.ALARM_TIME };
String select = "((" + DailyAlarmTable.ALARM_MEDNUM + " NOTNULL) AND ("
+ DailyAlarmTable.ALARM_MEDNUM + " = " + id + "))";
loader = new CursorLoader(getActivity(), baseUri,
alarmProjection, select, null,
DailyAlarmTable.ALARM_TIMESTAMP + " ASC");
break;
}
return loader;
}
/**
* Customizes the various TextViews in the layout to match
* the values pulled from the MedTable, or swaps the alarm cursor
* into the adapter.
*/
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
switch(loader.getId()) {
case 0:
setUpMedDetails(data);
break;
case 1:
Log.d("MedManager", "Alarm finished loading");
/*
* these lines are commented out because their presence causes
* the app to crash.
*/
/*
boolean isEmpty = data.getCount() < 1;
if(isEmpty) {
Log.d("MedManager", "No results");
}
*/
mAdapter.swapCursor(data);
break;
}
}
#Override
public void onLoaderReset(Loader<Cursor> arg0) {
// TODO Auto-generated method stub
if(arg0.getId() == 1) {
mAdapter.swapCursor(null);
}
}
EDIT: For completeness' sake, and because the possibility always exists that I'm simply a massive idiot overlooking something obvious, here's the code by which I add alarms into the table:
/**
* This function will turn the hour and day into an "HH:mm AM/PM" string,
* calculate the timestamp, and then inserts them into the table.
*/
#Override
public void onTimePicked(int hourOfDay, int minute) {
Log.d("MedManager", "onTimePicked triggered");
// Convert the hour and minute into a string
String alarmString = formatAlarmString(hourOfDay, minute);
// Convert the hour and minute into a timestamp
long alarmTimestamp = getAlarmTimestamp(hourOfDay, minute);
// Define the URI to receive the results of the insertion
Uri newUri = null;
// Define a contentValues object to contain the new Values
ContentValues mValues = new ContentValues();
// Add medId;
long medId = getIntent().getLongExtra(MedDetailFragment.ARG_MED_ID, 0);
mValues.put(DailyAlarmTable.ALARM_MEDNUM, medId);
// Add the timestamp
mValues.put(DailyAlarmTable.ALARM_TIMESTAMP, alarmTimestamp);
// Add the time string
mValues.put(DailyAlarmTable.ALARM_TIME, alarmString);
// Insert the new alarm
Toast.makeText(getApplicationContext(), "medNum = " + medId, Toast.LENGTH_SHORT).show();
Toast.makeText(getApplicationContext(), "time = " + alarmString, Toast.LENGTH_SHORT).show();
newUri = getContentResolver().insert(AlarmProvider.CONTENT_URI, mValues);
String uriStr = newUri.toString();
Toast.makeText(getApplicationContext(), "Uri = " + uriStr, Toast.LENGTH_SHORT).show();
}
As requested, here's my AlarmProvider class.
package com.gmail.jfeingold35.medicationmanager.alarmprovider;
import java.util.Arrays;
import java.util.HashSet;
import com.gmail.jfeingold35.medicationmanager.database.AlarmDatabaseHelper;
import com.gmail.jfeingold35.medicationmanager.database.DailyAlarmTable;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class AlarmProvider extends ContentProvider {
// Database
private AlarmDatabaseHelper database;
// Used for the UriMatcher
private static final int ALARMS = 10;
private static final int ALARM_ID = 20;
private static final String AUTHORITY = "com.gmail.jfeingold35.medicationmanager.alarmprovider";
private static final String BASE_PATH = "medicationmanager";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
+ "/" + BASE_PATH);
public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE
+ "/alarms";
public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE
+ "/alarm";
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, BASE_PATH, ALARMS);
sUriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", ALARM_ID);
}
#Override
public boolean onCreate() {
database = new AlarmDatabaseHelper(getContext());
return false;
}
/**
* Perform a query from the alarm database
*/
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Using SQLiteQueryBuilder instead of the query() method
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
// Check if the caller requested a column which doesn't exist
checkColumns(projection);
// Set the table
queryBuilder.setTables(DailyAlarmTable.TABLE_ALARM);
int uriType = sUriMatcher.match(uri);
switch(uriType) {
case ALARMS:
break;
case ALARM_ID:
// Adding the ID to the original query
queryBuilder.appendWhere(DailyAlarmTable.ALARM_ID + "="
+ uri.getLastPathSegment());
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
SQLiteDatabase db = database.getWritableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
// Make sure that potential listeners are getting notified
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return null;
}
/**
* Delete from the alarm database
*/
public int delete(Uri uri, String selection, String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
SQLiteDatabase db = database.getWritableDatabase();
int rowsDeleted = 0;
switch(uriType) {
case ALARMS:
rowsDeleted = db.delete(DailyAlarmTable.TABLE_ALARM, selection,
selectionArgs);
break;
case ALARM_ID:
String id = uri.getLastPathSegment();
if(TextUtils.isEmpty(selection)) {
rowsDeleted = db.delete(DailyAlarmTable.TABLE_ALARM,
DailyAlarmTable.ALARM_ID + "=" + id, null);
} else {
rowsDeleted = db.delete(DailyAlarmTable.TABLE_ALARM,
DailyAlarmTable.ALARM_ID + "=" + id + " and " + selection,
selectionArgs);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsDeleted;
}
#Override
public String getType(Uri uri) {
return null;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
int uriType = sUriMatcher.match(uri);
SQLiteDatabase db = database.getWritableDatabase();
long id = 0;
switch(uriType) {
case ALARMS:
id = db.insert(DailyAlarmTable.TABLE_ALARM, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_PATH + "/" + id);
}
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
SQLiteDatabase db = database.getWritableDatabase();
int rowsUpdated = 0;
switch(uriType) {
case ALARMS:
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
selection,
selectionArgs);
break;
case ALARM_ID:
String id = uri.getLastPathSegment();
if(TextUtils.isEmpty(selection)) {
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
DailyAlarmTable.ALARM_ID + "=" + id,
null);
} else {
rowsUpdated = db.update(DailyAlarmTable.TABLE_ALARM,
values,
DailyAlarmTable.ALARM_ID + "=" + id + " and " + selection,
selectionArgs);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
/**
* Confirms that the columns the user requested exist.
* #param projection
*/
public void checkColumns(String[] projection) {
String[] available = { DailyAlarmTable.ALARM_ID,
DailyAlarmTable.ALARM_MEDNUM,
DailyAlarmTable.ALARM_TIMESTAMP,
DailyAlarmTable.ALARM_TIME };
if(projection != null) {
HashSet<String> requestedColumns = new HashSet<String>(Arrays.asList(projection));
HashSet<String> availableColumns = new HashSet<String>(Arrays.asList(available));
// Check if all columns which are requested are available
if(!availableColumns.containsAll(requestedColumns)) {
throw new IllegalArgumentException("Unknown columsn in projection");
}
}
}
}
Oh oh, okay I found that you return null in your query method of AlarmProvider class :)). Let's return cursor for this
If the onLoadFinished() method passes you a null cursor, then that means that the ContentProvider's query() method has returned null. You need to fix your query() method so that it doesn't return null in this case.

SQLite same result after update

Strange behaviour of SQLite update in ContentProvider.
Update method:
#Override
public int update(Uri uri, ContentValues updateValues, String whereClause, String[] whereValues) {
SQLiteDatabase db = TasksContentProvider.dbHelper.getWritableDatabase();
int updatedRowsCount;
String finalWhere;
db.beginTransaction();
// Perform the update based on the incoming URI's pattern
try {
switch (uriMatcher.match(uri)) {
case MATCHER_TASKS:
updatedRowsCount = db.update(TasksTable.TABLE_NAME, updateValues, whereClause, whereValues);
break;
case MATCHER_TASK:
String id = uri.getPathSegments().get(TasksTable.TASK_ID_PATH_POSITION);
finalWhere = TasksTable._ID + " = " + id;
// if we were passed a 'where' arg, add that to our 'finalWhere'
if (whereClause != null) {
finalWhere = finalWhere + " AND " + whereClause;
}
updatedRowsCount = db.update(TasksTable.TABLE_NAME, updateValues, finalWhere, whereValues);
break;
default:
// Incoming URI pattern is invalid: halt & catch fire.
throw new IllegalArgumentException("Unknown URI " + uri);
}
} finally {
db.endTransaction();
}
if (updatedRowsCount > 0) {
DVSApplication.getContext().getContentResolver().notifyChange(uri, null);
}
return updatedRowsCount;
}
Query method:
#Override
public Cursor query(Uri uri, String[] selectedColumns, String whereClause, String[] whereValues, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// Choose the projection and adjust the "where" clause based on URI pattern-matching.
switch (uriMatcher.match(uri)) {
case MATCHER_TASKS:
qb.setTables(TasksTable.TABLE_NAME);
qb.setProjectionMap(tasksProjection);
break;
// asking for a single comic - use the rage comics projection, but add a where clause to only return the one
// comic
case MATCHER_TASK:
qb.setTables(TasksTable.TABLE_NAME);
qb.setProjectionMap(tasksProjection);
// Find the comic ID itself in the incoming URI
String taskId = uri.getPathSegments().get(TasksTable.TASK_ID_PATH_POSITION);
qb.appendWhere(TasksTable._ID + "=" + taskId);
break;
case MATCHER_TASK_COMMENTS:
qb.setTables(TaskCommentsTable.TABLE_NAME);
qb.setProjectionMap(taskCommentsProjection);
break;
case MATCHER_TASK_COMMENT:
qb.setTables(TaskCommentsTable.TABLE_NAME);
qb.setProjectionMap(taskCommentsProjection);
String commentId = uri.getPathSegments().get(TaskCommentsTable.TASK_COMMENT_ID_PATH_POSITION);
qb.appendWhere(TaskCommentsTable._ID + "=" + commentId);
break;
default:
// If the URI doesn't match any of the known patterns, throw an exception.
throw new IllegalArgumentException("Unknown URI " + uri);
}
SQLiteDatabase db = TasksContentProvider.dbHelper.getReadableDatabase();
// the two nulls here are 'grouping' and 'filtering by group'
Cursor cursor = qb.query(db, selectedColumns, whereClause, whereValues, null, null, sortOrder);
// Tell the Cursor about the URI to watch, so it knows when its source data changes
cursor.setNotificationUri(DVSApplication.getContext().getContentResolver(), uri);
return cursor;
}
Trying to update and row.
int affectedRowsCount = provider.update(Uri.parse(TasksTable.CONTENT_URI.toString() + "/"+ taskId), task.getContentValues(), null, null);
affectedRowsCount is eqaul to 1
Check if row is updated
Cursor cs = provider.query(TasksTable.CONTENT_URI, new String[] {TasksTable.TASK_STATE_VALUE}, TasksTable._ID +" = ?", new String[] {String.valueOf(taskId)}, null);
if(cs.moveToFirst()) {
String state = cs.getString(cs.getColumnIndex(TasksTable.TASK_STATE_VALUE));
}
state is the same as before update. Though update went succesful because affectedRowsCount is equal to 1 but selecting by the same id the same row seems that row wasn't updated at all.
In your update method you are using a transaction, but you never set the result as successful, so everytime you reach db.endTransaction() a rollback is performed. That's why your update isn't stored.
The changes will be rolled back if any transaction is ended without
being marked as clean (by calling setTransactionSuccessful). Otherwise
they will be committed.
You need to use
db.setTransactionSuccessful();
when your update is finished without errors. In your code, it should be after both your db.update.

Fill ListView from a cursor in Android

I've spent a lot of hours searching and reading similar posts, but none of them seem to truly reflect my problem and thus I haven't found anything that works for me.
I've got a database, on which I perform a query, the results of which are stored in a cursor. There's two things to that:
-the query is performed everytime a certain button is pressed (thus the query is inside the OnClickListener for that Button)
-the query returns two different columns with String values, which must be treated separately (one column stores the names which must be shown in the ListView, the other stores the paths to the image associated toa row)
My problem is, I try to create a String[] which I need to pass to the ArrayAdapter creator for the ListView, but trying to assign it a size of Cursor getCount() crashes my activity. I hope the code will be more of an explanation:
OnClickListener searchListener = new OnClickListener() {
public void onClick(View v) {
CardDatabaseOpenHelper helper = new
CardDatabaseOpenHelper(DeckEditorScreen1.this);
SQLiteDatabase db = helper.getReadableDatabase();
String columns[] = {"name","number","path"};
Cursor c = db.query("cards", columns, null, null, null, null, "number");
int count = c.getCount();
String[] resultNameStrings;
if (count != 0) resultNameStrings = new String[count];
else {resultNameStrings = new String[1]; resultNameStrings[1] = "No results";}
// This is the offending code
//Note that if I assign fixed values to resutNameStrings, the code works just
//fine
for (int i = 0; i < count; ++i) {
c.moveToNext();
int col = c.getColumnIndex("name");
String s = c.getString(col);
//Ideally here I would to something like:
//resultNameStrings[i] = s;
col = c.getColumnIndex("number");
int conv = c.getInt(col);
col = c.getColumnIndex("path");
String s2 = c.getString(col);
}
db.close();
ArrayAdapter<?> searchResultItemAdapter = new ArrayAdapter<String>
(DBScreen.this,
R.layout.search_result_item,
resultNameStrings);
ListView searchResultList = (ListView)
DBScreen.this.findViewById(R.id.search_result_list);
searchResultList.setAdapter(searchResultItemAdapter);
}
};
Button search_button = (Button) findViewById(R.id.search_button);
search_button.setOnClickListener(searchListener);
EDITED twice :)
do it in "Android Way" ...
first use CursorAdapter (fx.: SimpleCursorAdapter with overrided
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
Cursor cursor = managedQuery(MobileTraderProvider.CONTENT_URI,
null, null, new String[] { constraint.toString() }, null);
return cursor;
}
then
customAdapter.getFilter().filter(filterText)
// it will call runQueryOnBackgroundThread
second use ContentProvider(it will manage curosors for you ... it will even requery if data changed)
EDIT:
first really use my advice
second before
for (int i = 0; i < count; ++i) {
c.moveToNext();
//...
add c.moveToFirst();
thrid: use
if(c.moveToNext())
{
int col = c.getColumnIndex("name");
//..... rest goes here
}
SECOND EDIT:
MyProvider.java
public class MyProvider extends ContentProvider {
static final String LTAG = "MyAppName";
public static final Uri CONTENT_URI = Uri.parse("content://my.app.Content");
static final int CARDS = 1;
static final int CARD = 2;
public static final String CARDS_MIME_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/Cards";
public static final String CARD_MIME_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/Cards";
static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
static final HashMap<String, String> map = new HashMap<String, String>();
static {
//static "Constructor"
matcher.addURI(Constants.AUTHORITY, "Cards", LISTS);
matcher.addURI(Constants.AUTHORITY, "Cards/*", LIST);
map.put(BaseColumns._ID, "ROWID AS _id");
map.put(Tables.Cards.C_NAME, Tables.Cards.C_NAME);
map.put(Tables.Cards.C_NUMBER, Tables.Cards.C_NUMBER);
map.put(Tables.Cards.C_PATH, Tables.Cards.C_PATH);
}
private CardDatabaseOpenHelper mDB;
#Override
public boolean onCreate() {
try {
mDB = new CardDatabaseOpenHelper(getContext());
} catch (Exception e) {
Log.e(LTAG, e.getLocalizedMessage());
}
return true;
}
public int delete(Uri uri, String selection, String[] selectionArgs) {
String table = null;
switch (matcher.match(uri)) {
case CARD:
//overriding selection and selectionArgs
selection = "ROWID=?";
selectionArgs = new String[] { uri.getPathSegments().get(1) };
table = uri.getPathSegments().get(0);
break;
case CARDS:
//this version will delete all rows if you dont provide selection and selectionargs
table = uri.getPathSegments().get(0);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
int ret = mDB.getWritableDatabase().delete(table, selection, selectionArgs);
getContext().getContentResolver().notifyChange(Uri.withAppendedPath(CONTENT_URI, table), null);
return ret;
}
#Override
public String getType(Uri uri) {
switch (matcher.match(uri)) {
case CARDS:
return CARDS_MIME_TYPE;
case CARD:
return CARD_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
String table, rowid;
switch (matcher.match(uri)) {
case CARD:
//overriding selection and selectionArgs
selection = "ROWID=?";
selectionArgs = new String[] { uri.getPathSegments().get(1) };
table = uri.getPathSegments().get(0);
break;
case CARDS:
//this version will update all rows if you dont provide selection and selectionargs
table = uri.getPathSegments().get(0);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
int ret = mDB.getWritableDatabase().update(table, values, selection, selectionArgs);
getContext().getContentResolver().notifyChange(Uri.withAppendedPath(CONTENT_URI, table), null);
return ret;
}
public Uri insert(Uri uri, ContentValues values) {
String table = null;
switch (matcher.match(uri)) {
case CARDS:
table = uri.getPathSegments().get(0);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
mDB.getWritableDatabase().insert(table, null, values);
getContext().getContentResolver().notifyChange(Uri.withAppendedPath(CONTENT_URI, table), null);
return null;
}
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
switch (matcher.match(uri)) {
case CARDS:
builder.setTables(uri.getPathSegments().get(0));
break;
case CARD:
builder.setTables(uri.getPathSegments().get(0));
selection = "ROWID=?";
selectionArgs = new String[] { uri.getPathSegments().get(1) };
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
builder.setProjectionMap(map);
Cursor cursor = builder.query(mDB.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
if (cursor == null) {
return null;
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
}
CardCursorAdapter.java
class CardCursorAdapter extends SimpleCursorAdapter {
public MyCursorAdapter(Context context, int layout, Cursor c,
String[] from, int[] to) {
super(context, layout, c, from, to);
}
#Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
//search in cards.name
String selection = Tables.Cards.C_NAME + " LIKE ?";
String[] selectionArgs = new String[] {"%" + constraint.toString() + "%"};
Cursor cursor = managedQuery(Uri.withAppendedPath(MyProvider.CONTENT_URI, Tables.Cards.Name),
getCursor().getColumnNames(), selection, selectionArgs, null);
return cursor;
}
}
Tables.java
public static class Tables {
//table definition
public static interface Cards {
public static final String NAME = "cards";
public static final String C_NAME = "name";
public static final String C_NUMBER = "number";
public static final String C_PATH = "path";
}
//other tables go here
}
AndroidManifest.xml
</manifest>
</application>
<!-- ....... other stuff ....... -->
<provider android:name="MyProvider" android:authorities="my.app.Content" />
</application>
</manifest>
then in activity
onCreate(...){
listView.setAdapter(new CardCursorAdapter(this, R.layout.listrow,
managedQuery(Uri.withAppendedPath(MyProvider.CONTENT_URI, Tables.Cards.NAME),
new String[] { BaseColumns._ID, Tables.Cards.C_NAME, Tables.Cards.C_NUMBER, Tables.Cards.C_PATH },
null,null, number),
new String[] { Tables.Cards.C_NAME, Tables.Cards.C_NUMBER, Tables.Cards.C_PATH },
new int[] { R.id.tName, R.id.tNumber, R.id.tPath }));
}
OnClickListener searchListener = new OnClickListener() {
public void onClick(View v) {
DeckEditorScreen1.this.listView.getAdapter().getFilter().filter("text for search in name column of card table set me to empty for all rows");
}
}
Ok, I've done some testing and I think I know what was the problem. Java allows constructs like:
String[] whatever;
if (something) whatever = new String[avalue];
else whatever = new String[anothervalue];
The crash occurs if you don't assign a concrete value to each and every field whatever[i]. The rest of the code is now just fine, though I've added Selvin's correction
if (c.moveToNext) ...
c.moveToFirst() is not correctly used in my case, as the for iterates count times. If you perform a moveToFirst first, you're always missing the first element pointed by the cursor.

Categories

Resources