I've updated my device to Android ICS (4.0.3) I have an activity which contains a list view filled in with data read from a database. The data are arranged in the listview using an extension of the ResourceCursorAdapter.
Once the list has been loaded and shown on the screen I've pressed the home screen in order to bring up the home screen. Then I've recovered my application from the recents (long pressing the home screen) and suddenly I got the following exception:
05-10 15:49:17.925: E/AndroidRuntime(10721): Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.database.BulkCursorToCursorAdaptor.requery(BulkCursorToCursorAdaptor.java:144)
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.database.CursorWrapper.requery(CursorWrapper.java:186)
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.app.Activity.performRestart(Activity.java:4505)
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.app.Activity.performResume(Activity.java:4531)
05-10 15:49:17.925: E/AndroidRuntime(10721): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2446)
I've read about the behavior of the OS in my case and the cursor seem to be invalidated. The problem is that despite I've registered a DataSetObserver the method onInvalidated is never called, and before the crash no Activity's method ( like onResume, onRestart ) is called as well. The code even didn't reach the bindView in the Adapter.
Could you please help me with that? I can provide more info and relevant code if you need.
Thanks in advance
Here the code, sorry if it is a mess but I'm just making it work before fine tuning:
public class CallLogsList extends Activity implements Runnable,
OnItemClickListener {
// ... various declaration here
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case CALLLOGS_LOAD_DONE:
loadCurrCallLogsList(true);
break;
case SHOW_ALL_LOG:
case SHOW_MISSED_LOG:
case SHOW_OUTGOING_LOG:
case SHOW_INCOMING_LOG:
// - set the adapter
if (null == mCad) {
// the first time the adapter need to be called
mCad = new CallLogsCursorAdapter(mContext, mDataCursor);
mCallLogsListView.setAdapter(mCad);
} else {
mCad.changeCursor(mDataCursor);
mCad.notifyDataSetChanged();
}
break;
} // end of switch ctrl structure
return;
} // end of method handleMessage
}; // end of Handler object
/**
* The following inner class implements the custom adapter to contain the
* call log entries as read from the database
*
*/
class CallLogsCursorAdapter extends ResourceCursorAdapter {
Cursor mCallLogCursor = null;
CallLogDataSetObserver mLogDataObserver = null;
/**
* Class constructor
*
* @param context
* @param c
*/
public CallLogsCursorAdapter(Context context, Cursor c) {
super(context, R.layout.recent_calls_list_item, c);
mLogDataObserver = new CallLogDataSetObserver();
mCallLogCursor = c;
mCallLogCursor.registerDataSetObserver(mLogDataObserver);
return;
} // end of class constructor
/**
* This method binds an existing view to the data pointed to by the
* cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// ... bind data to the view here
} // end of method bindView
/**
* This method inflates the new view from the specified resource Such
* resource has been passed to the super class in the call at the parent
* class constructor we did in this derived class
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = super.newView(context, cursor, parent);
// ... create the view
return view;
} // end of method newView
/**
/**
* The data set observer for the data used by this adapter
*/
private class CallLogDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
return;
} // end of method onChanged
@Override
public void onInvalidated() {
if( null != mCallLogCursor ) {
// TODO: Remove this call coz the UI can get stuck
// if the call log is too long. Just ask for a new
// cursor asynchronously
mCallLogCursor.requery();
}
return;
} // end of method onInvalidated
} // end of class inner class CallLogDataSetObserver
} // end of class CallLogsCursorAdapter
/**
* This method is called the first time the activity is created
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(THIS_FILE, "Enter onCreate");
super.onCreate(savedInstanceState);
// ... initialization code here
loadCurrCallLogsList(false);
return;
} // end of method onCreate
/**
* This method loads the current communication list
*/
private synchronized void loadCurrCallLogsList(final boolean fromPullDown) {
if (false == fromPullDown) {
showLoadingView(true);
}
// start the loader thread
Thread loader = new Thread(this);
loader.start();
return;
} // end of method loadCurrCommunicationsList
/**
* This method is called when the activity is going to be destroyed
*/
@Override
protected void onDestroy() {
if (null != database) database.close();
database = null;
if (mDataCursor != null) mDataCursor.close();
mDataCursor = null;
// call the super class onDestroy method
super.onDestroy();
}
/**
* This method create the menu for the activity
*
* @param menu
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.in_call_log_list_menu, menu);
return super.onCreateOptionsMenu(menu);
} // end of method onCreateOptionsMenu
/**
* This method is called when the menu is going to be shown
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (false == CordlessApplication.getInstance().canUseJungoApi()) {
menu.findItem(R.id.menu_item_edit).setEnabled(false);
} else {
// enable the edit item in the menu if the list is not empty
menu.findItem(R.id.menu_item_edit).setEnabled(
null != mCad && !mCad.isEmpty());
}
return super.onPrepareOptionsMenu(menu);
} // end of method onPrepareOptionsMenu
/**
* This method is called when the user click on an item in the menu
*
* @param item
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
item.setChecked(true);
// Handle item selection
switch (item.getItemId()) {
case R.id.sub_menu_item_all:
mCurrentLogView = CURR_LIST_ALL_LOG;
mListHeader.setText(R.string.all_calls_log_header_txt);
loadCurrCallLogsList(false);
break;
case R.id.sub_menu_item_in_only:
mCurrentLogView = CURR_LIST_INCOMING_LOG;
mListHeader.setText(R.string.received_calls_log_header_txt);
loadCurrCallLogsList(false);
break;
case R.id.sub_menu_item_out_only:
mCurrentLogView = CURR_LIST_OUTGOING_LOG;
mListHeader.setText(R.string.dialled_calls_log_header_txt);
loadCurrCallLogsList(false);
break;
case R.id.sub_menu_item_miss_only:
mCurrentLogView = CURR_LIST_MISSED_LOG;
mListHeader.setText(R.string.missed_calls_log_header_txt);
loadCurrCallLogsList(false);
break;
case R.id.menu_item_edit:
startModifyActivity();
break;
default:
return super.onOptionsItemSelected(item);
}
return (true);
} // end of method onOptionsItemSelected
/**
* This method is called when the user comes back to this activity from a
* sub activity
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (Activity.RESULT_OK != resultCode)
return;
switch (requestCode) {
case DialingUtils.REQ_CODE_ADD_NEW_CONTACT: // pass through
case DialingUtils.REQ_CODE_UPDATE_EXISTING_CONTACT:
// refresh the call log list
mCad.getCursor().requery();
mCad.notifyDataSetChanged();
break;
case DialingUtils.REQ_CODE_PICK_CONTACT:
DialingUtils.updateExistingContact(this, data.getData(),
mCallerInfo.mPhoneNumber, true);
break;
}
} // end of method onActivityResult
/**
/**
* This method load a filter version of the call logs
*
* @param filter
*/
private void loadFilteredData(final int filter) {
if( null != mDataCursor ) mDataCursor.close();
mDataCursor = null;
// see whether it is needed to recover the database
if (null == database) {
database = new DBAdapter(mContext);
database.open();
}
// read all the call logs from the database
mDataCursor = database.getFilteredCallLogs(filter);
return;
} // end of method loadFilterData
/**
* This method is called when the user press a key on the device We use this
* method to handle the press on the back key
*/
@Override
/**
* This method is called in order to load the data from the
* local database in a separated thread
*/
@Override
public synchronized void run() {
Looper.prepare();
synchronized (MyConstants.mCallLogsMutex) {
switch (mCurrentLogView) {
case CURR_LIST_ALL_LOG:
loadFilteredData(0);
mHandler.sendEmptyMessage(SHOW_ALL_LOG);
break;
case CURR_LIST_MISSED_LOG:
loadFilteredData(CallLog.Calls.MISSED_TYPE);
mHandler.sendEmptyMessage(SHOW_MISSED_LOG);
break;
case CURR_LIST_OUTGOING_LOG:
loadFilteredData(CallLog.Calls.OUTGOING_TYPE);
mHandler.sendEmptyMessage(SHOW_OUTGOING_LOG);
break;
case CURR_LIST_INCOMING_LOG:
loadFilteredData(CallLog.Calls.INCOMING_TYPE);
mHandler.sendEmptyMessage(SHOW_INCOMING_LOG);
break;
}
} // end of synch block
} // end of method run
} // end of class CallLogsList