I'm setting up my app so that people can create groups of their friends. When a group is created, it writes 2 tables to the SQL database. The first table has a group name and a group id. The second table has 2 columns, a group id and a user id. This is working fine.
However, now I want to be able to read from the database. I'm using a listview fragment with a cursorloader but I'm having trouble getting the information to display. I want to list all the group names from the first table in my list view.
My problem is that, when I first used the cursorloader to list my contacts, I was using a Uri from the content provider in the onCreateLoader method. Specifically I had CONTENT_URI from the ContactsContracts.Contacts class.
Example of cursorloader with contentprovider:
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri contentUri = ContactsContract.Contacts.CONTENT_URI;
return new CursorLoader(getActivity(),contentUri,PROJECTION,SELECTION,ARGS,ORDER);
}
However, without using a content provider, I don't know what to put in the onCreateLoader method because return new CursorLoader(...) requires a Uri in the second argument.
Any suggestion on how I might be able to display my database data in a listview?
fragment class code:
public class GroupListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
CursorAdapter mAdapter;
private OnItemSelectedListener listener;
private static final String[] PROJECTION ={GroupContract.GroupDetails.COLUMN_NAME_GROUP_NAME};
private static final String SELECTION = null;
final String[] FROM = {GroupContract.GroupDetails.COLUMN_NAME_GROUP_NAME};
final int[] TO = {android.R.id.text1};
private static final String[] ARGS = null;
private static final String ORDER = null;
private Cursor c;
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_1,null,FROM,TO,0 );
ReadDBAsync readDB = new ReadDBAsync();
readDB.execute();
}
#Override
public void onActivityCreated(Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
setListAdapter(mAdapter);
getLoaderManager().initLoader(0,null,this);
}
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri contenturi = Uri.parse("content://preamble.oneapp");
Uri tableuri = Uri.withAppendedPath(contenturi,GroupContract.GroupDetails.TABLE_NAME);
return new CursorLoader(getActivity(),tableuri,PROJECTION,SELECTION,ARGS,ORDER);
}
#Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
#Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
mAdapter.swapCursor(null);
}
private class ReadDBAsync extends AsyncTask<Void,Void,String> {
#Override
protected String doInBackground(Void... voids) {
ContractDBHelpers mDBHelper = new ContractDBHelpers(getActivity());
SQLiteDatabase db = mDBHelper.getReadableDatabase();
String returnvalue = "database read";
c = db.query(GroupContract.GroupDetails.TABLE_NAME,PROJECTION,null,null,null,null,null);
return returnvalue;
}
#Override
protected void onPostExecute(String result){
Toast.makeText(getActivity(), result, Toast.LENGTH_LONG).show();
}
}
}
Android Guide suggests to create a ContentProvider when you want to share your data with other applications. If you don't need this, you can just override method loadInBackgroud() of the CursorLoader class. For example write like this in your onCreateLoader:
return new CursorLoader( YourContext, null, YourProjection, YourSelection, YourSelectionArgs, YourOrder )
{
#Override
public Cursor loadInBackground()
{
// You better know how to get your database.
SQLiteDatabase DB = getReadableDatabase();
// You can use any query that returns a cursor.
return DB.query( YourTableName, getProjection(), getSelection(), getSelectionArgs(), null, null, getSortOrder(), null );
}
};
These are the steps to create a cursorloader in a list fragment
1) Create a class extending SQLiteOpenHelper and override onCreate and onUpgrade to create your tables.
2) Create a class extending ContentProvider and create the URIs to access your database. Refer http://developer.android.com/guide/topics/providers/content-providers.html. Add your URIs to the URIMatcher which you use in onCreate, onUpdate, query, etc (overridden methods) to match the URI. Refer http://developer.android.com/reference/android/content/UriMatcher.html
3) In the insert method call getContext().getContentResolver().notifyChange(uri, null). In the query method call setNotificationUri(ContentResolver cr, Uri uri) before returning the content provider for the insertion change to reflect automatically to your loader. (https://stackoverflow.com/a/7915117/936414).
4) Give that URI in onCreateLoader.
Note:
Without a content provider, automatic refreshing of changes to the list is not feasible as of the current android version. If you don't want to have your contentprovider visible, set exported attribute in manifest to false. Or you can implement your custom CursorLoader as in https://stackoverflow.com/a/7422343/936414 to retrieve data from the database. But in this case automatic refreshing of data is not possible
Related
I want to pass my Listview clicked item ID to other activity using intent. HoW could I do this?
You get the id and add it to the Intent as an Extra using the respective (long) putExtra, with a suitable key (first parameter).
In the other Activity you get the Intent and then get the value from the Extra using the getLongExtra using the same key (first parameter) noting that the default value should be a value (e.g. -1) that will not be an id.
Working Example
The following is a working example :-
The Database Helper DBOpenHelper.java
public class DBOpenHelper extends SQLiteOpenHelper {
public static final String DBNAME = "notes.db";
public static final int DBVERSION = 1;
public static final String TBL_NOTE = "note_table";
public static final String COL_NOTE_ID = BaseColumns._ID;
public static final String COL_NOTE_TEXT = "note_text";
private final String create_table = "CREATE TABLE IF NOT EXISTS " + TBL_NOTE + "(" +
COL_NOTE_ID + " INTEGER PRIMARY KEY, " +
COL_NOTE_TEXT + " TEXT " +
")";
private static final String drop_table = "DROP TABLE IF EXISTS " + TBL_NOTE;
public DBOpenHelper(Context context) {
super(context, DBNAME, null, DBVERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(create_table);
}
public void onUpgrade(SQLiteDatabase db, int version_old, int version_new) { }
public long addNote(String note) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put("NOTE_TEXT",note);
return db.insert("note_table",null,cv);
}
public Cursor getAllNotes() {
SQLiteDatabase db = this.getWritableDatabase();
return db.query(TBL_NOTE,null,null,null,null,null,null);
}
}
An instantiated instance of this is used to :
open the database (in the example it is named notes.db), it will create the database if it doesn't exist.
add data via the addNote method.
return a Cursor containing all the Notes via the getAllNotes method.
The initial activity MainActivity.java :-
public class MainActivity extends AppCompatActivity {
private ListView mListView;
private SimpleCursorAdapter MSCA;
private Cursor mCsr;
private DBOpenHelper mDBHelper;
public static final String INTENTEXTRAKEY_NOTE_ID = "iek_noteid";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDBHelper = new DBOpenHelper(this);
mListView = findViewById(R.id.listView);
addSomeTestData(); //Adds a row every time the App is run (for testing)
ViewData();
}
private void ViewData(){
mCsr = mDBHelper.getAllNotes();
if (MSCA == null) {
MSCA = new SimpleCursorAdapter(
this,
android.R.layout.simple_list_item_1,
mCsr,
new String[]{DBOpenHelper.COL_NOTE_TEXT},
new int[]{android.R.id.text1},
0);
mListView.setAdapter(MSCA);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent i = new Intent(MainActivity.this,OtherActivity.class);
i.putExtra(INTENTEXTRAKEY_NOTE_ID,id);
startActivity(i);
}
});
} else {
MSCA.swapCursor(mCsr);
}
}
private void addSomeTestData() {
if(DatabaseUtils.queryNumEntries(mDBHelper.getWritableDatabase(),DBOpenHelper.TBL_NOTE) < 1) {
mDBHelper.addNote("My Note");
mDBHelper.addNote("Another Note");
mDBHelper.addNote("Yet Another Note");
mDBHelper.addNote("And so on.");
}
}
#Override
protected void onDestroy() {
super.onDestroy();
mCsr.close();
}
#Override
public void onResume()
{
super.onResume();
ViewData();
}
}
This activity :-
Adds some notes to the database (if none already exist)
Lists all of the notes in the database
sets a onItemClickListener to call the activity OtherActivity passing the id via an IntenExtra.
Notes
A CursorAdpater (SimpleCursorAdapter) in this case is ideally suited as the id is passed to the onItemClick method by the listener. NOTE this requires a column, named specifically _id (as per BaseColumns._ID) and that the column is an alias of the rowid column.
The onDestroy method has been overridden to close the Cursor when done with it (i.e. when the Activity is detsroyed). This isn't so important in the initial actiivty as it's lifetime would typically be for the entire App (closing the Cursor in hierarchically lower Activities is more important).
The ViewData method manages a single instance of the adapter (i.e. creating it only when it hasn't been instantiated) and also refreshing of the ListView. Hence why it is called in the overridden onResume method. i.e. another activity may add. delete or update the database so the ListView will updated upon return.
The activity called from the initial activity OtherActivity.java
public class OtherActivity extends AppCompatActivity {
TextView mNoteId;
Button mDone;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_other);
mNoteId = this.findViewById(R.id.noteid);
mDone = this.findViewById(R.id.done);
mDone.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
Intent i = this.getIntent();
long id = i.getLongExtra(MainActivity.INTENTEXTRAKEY_NOTE_ID,-1);
mNoteId.setText(String.valueOf(id));
}
}
This activity :-
sets the onClickListener of the button to return to the calling/invoking activity.
extracts the id from the Intent Extras passed to the activity from the calling activity and sets the noteid TextView with the extracted value (the passed id).
Result
Initially :-
Clicking Yet Another Note :-
As Yet Another Note was the third note added it's id is 3.
I have a big database which takes time to find needed information. So I decided to use RxJava to make this process asynchronous.
#Override
public void afterTextChanged(final Editable s) {
final String query = s.toString();
Observable.create(new Observable.OnSubscribe<Cursor>() {
#Override
public void call(Subscriber<? super Cursor> subscriber) {
subscriber.onNext(database.search(query));
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<Cursor>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
e.printStackTrace();
}
#Override
public void onNext(Cursor cursor) {
scAdapter.swapCursor(cursor);
}
});
}
But query is running on main thread: EditText where I entering text is freezing.
My question is how to run SQLite query asynchronously on background thread?
Probably this https://developer.android.com/reference/android/app/LoaderManager.html
will suite for you.
Besides, here is short implementation for you, but this is not RxJava.
Firstly, you need to implement LoaderManager.LoaderCallbacks<Cursor>, and usually this interface is implemented by Activity (or Fragment).
In onCreateLoader, a CursorLoader should be created and returned. Here is just an example with MyCursorLoader as descendant of CursorLoader, where you can perform connection to database and queries.
In onLoadFinished you have to treat cursor with results of query.
Please, consider the link to android.com, mentioned above.
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
#Override
protected void onResume() {
super.onResume();
// start loading data using LoaderManager of Activity
// third argument only has sense in this case
getLoaderManager().initLoader(0, null, this);
}
private static final String ACTIVITY_NAME = "main_activity";
private void treatCursorRow(Cursor cursor){
// treat record from cursor
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// this callback is called by LoaderManager in order to obtain CursorLoader
// here a new one loader is created
// created loader will be processed by LoaderManager
return new MyCursorLoader(this, ACTIVITY_NAME);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// this callback is called when loader finishes load cursor
// you don't need to destroy loader - just tread the data
if(data != null)
while(data.moveToNext())
treatCursorRow(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
// here you can do something
// relevant to cancelling of loading data
// in example, when you have an event that cancels current
// loading and restarts new one
}
class MyCursorLoader extends CursorLoader {
private static final String DATABASE_NAME = "my_database";
private static final int DATABASE_VERSION = 1;
private String name_param;
public MyCursorLoader(Context context, String activity_name) {
super(context);
name_param = activity_name;
}
#Override
public Cursor loadInBackground() {
// assuming, that we have implemented SQLiteOpenHelper
// to treat sqlite-database
MyDatabaseHelper dbh = new MyDatabaseHelper(
MainActivity.this,
DATABASE_NAME,
null,
DATABASE_VERSION
);
return dbh.getWritableDatabase().rawQuery(
"SELECT * FROM some_table WHERE name=?",
new String[]{ name_param }
);
}
}
}
Another way, is in using of ContentProvider https://developer.android.com/guide/topics/providers/content-providers.html .
In this way you can separate data layer and business logic. Your data access will be abstracted to uris.
Using ContentProvider, you define your queries within it and load data using Uri:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return getContentResolver().query(
YourContentProvider.SOME_URI,
null,
null,
null,
null
);
}
This is convenient way if you have more than one or two customers of your data (Activities or Fragments) - you will use just predefined uris rather repeating sql queries or creating many CursorLoaders descendands.
Moreover, ContentProvider may be used from outside your app if you want.
I'm new in android and I'm not sure if what I'm looking for possible, is it possible to update the listview content handled by the loader manager from a thread? if yes can you please show me how?
More details are below, I've removed many lines for brevity, please let me know if you need more details
The HandlerThread I'm using is this and where I need to notify the loader about the change to udpate the listview content
public void syncWithBackend(Context context) {
//Connect to the server over HTTP and get the latest data after receiving
//the GCM tickle message then save the result in DB
dbhelper.saveFm(id, artno, comment );
//Here is where I need to notify the loader or the list about the new change
// to view the new saved data
context.getContentResolver().notifyChange(uri, null);
}
The URI I'm using is belwo,
Uri uri = Uri.parse("sqlite://com.pack.android.and/posts");
my LoaderCallbacks is this
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new FeedListCursorLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cCursorAdapter adapter = new cCursorAdapter(getActivity(),
(fCursor) cursor);
setListAdapter(adapter);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
setListAdapter(null);
}
My Intent Service is this
protected void onHandleIntent(Intent intent) {
//Here I got the GCM message, call the thread to sync with the backend
hanlder1.queueHttp(this);
}
My AsyncTaskLoader is this
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
private Cursor cCursor;
public SQLiteCursorLoader(Context context) {
super(context);
}
//I'm overriding this from the Fragment to get the cursor after querying sqlite DB
protected abstract Cursor loadCursor();
#Override
public Cursor loadInBackground() {
Cursor cursor = loadCursor();
if (cursor != null) {
cursor.getCount();
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
return cursor;
}
#Override
public void deliverResult(Cursor data) {
Cursor oldCursor = cCursor;
cCursor = data;
if (isStarted()) {
super.deliverResult(data);
}
if (oldCursor != null && oldCursor != data && !oldCursor.isClosed()) {
oldCursor.close();
}
}
#Override
protected void onStartLoading() {
...
}
#Override
protected void onStopLoading() {
...
}
#Override
public void onCanceled(Cursor cursor) {
...
}
#Override
protected void onReset() {
...
}
Yes, but not quite as you expect.
You will not update the cursor, you will replace it. In fact, the LoaderManager will do it for you, automatically. It is almost unbelievably simple
When you create a cursor, in response to the request from your Loader, register it as a listener for a particular URI:
cursor.setNotificationUri(getContext().getContentResolver(), uri);
The URI can be anything you want. It represents (is the "name" of?) the data.
When your Loader is run by the LoaderManager, the LoaderManager gets the cursor that the Loader returns, before it hands it to you, in the callback. It registers as a listener, on that cursor.
If, at some point after that, you announce that a change has taken place, in the data that that URI represents, like this:
getContext().getContentResolver().notifyChange(uri, null);
The cursor (registered as a listener on that URI) will be notified. When the cursor is notified, the LoaderManager (which registered as a listener on the cursor) will be notified, and it will, automatically, re-run the Loader!
In your case, you would do the notification on that other thread...
If your onLoadFinished method simply swaps out the old cursor and swaps in the new one, your ListView will update by magic.
Some parts of Android are just awesome.
Edited to add sample code:
Here is some example code to demonstrate how this works. db is a reference to a SQLiteOpenHelper. The methods insert and query are simple db insert and query respectively.
First the insert code:
db.insert();
getContentResolver().notifyChange(DbHelper.URI, null);
Then the Loader:
private static class MagicLoader extends AsyncTaskLoader<Cursor> {
private final DbHelper db;
private volatile Cursor cursor;
private final ContentObserver obs = new ContentObserver(new Handler()) {
#Override public boolean deliverSelfNotifications() { return true; }
#Override public void onChange(boolean selfChange) { onContentChanged(); }
};
public MagicLoader(Context ctxt, DbHelper db) {
super(ctxt);
this.db = db;
}
#Override
protected void onStartLoading() {
if (null == cursor) { forceLoad(); }
else { deliverResult(cursor); }
}
#Override
public Cursor loadInBackground() {
cursor = db.query();
if (cursor != null) {
try {
cursor.setNotificationUri(
getContext().getContentResolver(),
DbHelper.URI);
cursor.getCount();
cursor.registerContentObserver(obs);
}
catch (RuntimeException ex) {
cursor.close();
throw ex;
}
}
return cursor;
}
};
Note that this would all be a lot easier if you were just using a ContentProvider.
I have a ListView activity which loads its data asynchronously from a SQLite database using a ContentProvider.
I want to test this activity but I don't want to use the database. Because I want it to be repeatable.
I'm trying to mock my content provider this way:
public class MockRecipeContentProvider extends MockContentProvider{
private List<HashMap<String, String>> results;
#SuppressWarnings("nls")
public MockRecipeContentProvider(){
super();
this.results = new ArrayList<HashMap<String,String>>();
//..populate this.result with som data
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
MatrixCursor mCursor = new MatrixCursor(new String[] {RecipeTable.COLUMN_ID,
RecipeTable.COLUMN_NAME,
RecipeTable.COLUMN_INGREDIENTS,
RecipeTable.COLUMN_DIRECTIONS});
for(int i =0; i<this.results.size(); i++){
mCursor.addRow(this.getDataAtPosition(i));
}
return mCursor;
}
}
And this is my test case:
public class MainActivityTest extends ActivityUnitTestCase<MainActivity>{
private static final int INITIAL_NUM_ITEMS = 2;
private MainActivity mActivity;
public MainActivityTest() {
super(MainActivity.class);
}
#Override
public void setUp() throws Exception{
super.setUp();
final MockContentResolver mockResolver = new MockContentResolver();
MockRecipeContentProvider mockContentProvider = new MockRecipeContentProvider();
mockResolver.addProvider(RecipeContentProvided.AUTHORITY,
mockContentProvider);
ContextWrapper mActivityContext = new ContextWrapper(
getInstrumentation().getTargetContext()) {
#Override
public ContentResolver getContentResolver() {
return mockResolver;
}
};
this.setActivityContext(mActivityContext);
startActivity(new Intent(mActivityContext, MainActivity.class), null, null);
this.mActivity = getActivity();
}
public void testDisplayCorrectNumOfItems(){
ListView lv = this.mActivity.getListView();
assertTrue(lv.getCount()==INITIAL_NUM_ITEMS);
}
}
The problem is that the MockRecipeContentProvider.query() is not called, neither the RecipeContentProvider.query(), so the listView is not populate.
What am I doing wrong?
On the other hand, I would like to write an integration test case, extending the ActivityInstrumentationTestCase2, I would like to test the life cycle etc, but I've read that mocking is not possible if you are extending ActivityInstrumentationTestCase2. So, how can I code a test like this without relaying on the database?
I don't want to use the database because I think that although I will use RenamingDelegatingContext it would cause that the tests wouldn't be repeatable at all, unless that I could drop the test database and recreate it in the setUp() method. Could this be done?
I had a working project, now I wanted to upgrade the layout and I'm trying to add Fragments (pre honeycomb), but now seems like I'm having some troubles connecting to my DB, and I'm getting NullPointerException:
ERROR/AndroidRuntime(27294):
at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:118)
ERROR/AndroidRuntime(27294):
at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:187)
I believe it has something to do with the Context i'm sending to the database constructor.
here is my class:
public class ShowFragment extends ListFragment {
ArrayList<String> results = new ArrayList<String>();
private SQLiteDatabase db;
WorkTrackdb workdb = new WorkTrackdb(getActivity());
private ViewGroup mRootView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Cursor c = getYears();
//some code here
}
Cursor getYears() {
db = workdb.getReadableDatabase();
String years[] = {"year"};
Cursor cursor = db.query(WorkTrackdb.TABLE_NAME, years, null,
null, null, null,"year DESC");
return cursor;
}
Try using SQLiteDatabase.openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags) instead.
I'm not sure maybe you need to instantiate WorkTrackdb workdb = new WorkTrackdb(getActivity()); inside the onCreate()