Changing device orientation failures. Working when using AutoCompleteTextView and CursorAdapter

Situation

I have an Activity with AutoCompleteTextView. As you type, AutoCompleteTextView finds matching names from your contacts and displays them in a list. If the orientation of the device changes when this list is displayed, the activity fails (error message after the source code).

Notes

I am developing for ICS 4.0.3 and testing on a Nexus S device. I am trying to follow the best practices for using LoaderManager for creating and manipulating cursors. I understand that the LoaderManager needs to preserve cursor data on orientation change ( http://developer.android.com/guide/topics/fundamentals/loaders.html#callback ), but that doesn't seem to be the case.

Since the CursorAdapter wants me to return the original, unfiltered cursor when the filtering constraint is too small to use, I:

  • Storing the cursor as a static Activity variable for use when there are no filter criteria (sorry for the wrong terminology. I'm new to Java).
  • Prevent cursors from closing the cursor after replacing it, unless it confirms that it is not the original cursor by comparing it to the saved cursor.

The problem is that the onLoadFinished LoaderManager callback gets called after the orientation has changed, but the cursor it is passing in (original cursor?) Was closed during the reorientation.

If I set up my activity to manage the orientation changes themselves by adding the following to the declaration activity

in the manifest:

android: configChanges = "orientation | Screen size"

the saved original cursor must be retained in all orientation changes (right?). While the app doesn't crash, another problem arises:

  • If I type some letters, change the device orientation and start deleting letters, as soon as I get to 1 or 0 letters, LogCat will give me a warning that I am trying to access the cursor after closing it.

It looks like my original cursor went away in this case too. I am guessing the app did not work because the onLoadFinished callback is not called when my activity is set up to manage the orientation changes themselves

My questions

  • Am I correct in assuming my cursor collapses when the device orientation changes?
  • How to save cursor and / or its data when changing device orientation?

Source

View - home.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <AutoCompleteTextView
        android:id="@+id/newPlayer_edit"
        android:inputType="text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:hint="Contact"
        android:singleLine="true" >
        <requestFocus />
    </AutoCompleteTextView>

</LinearLayout>

      

Activity - Home.java

public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {

// Constants
private static final String TAG = "HOME";
private static final boolean DEBUG = true;
public static final int LOADER_CONTACTS_CURSOR = 1;

// Variables
private AdapterContacts adapter;
public static Cursor originalCursor = null;


/**
 * Overrides
 */

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set the view
    setContentView(R.layout.home);

    // Initialize CursorAdapter
    adapter = new AdapterContacts(this, null, 0);

    // Attach CursorAdapter to AutoCompleteTextView field
    AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
    field.setAdapter(adapter);

    // Initialize Cursor using LoaderManager
    LoaderManager.enableDebugLogging(true);
    getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
}

@Override
public void onDestroy() {
    if (DEBUG) Log.i(TAG, "Destroying activity");
    super.onDestroy();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
    return new CursorLoader(this, ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
    // If no cursor has been loaded before, reserve this cursor as the original
    // It will be returned by the CursorAdapter when the filter constraint is null 
    if (originalCursor == null) {
        originalCursor = cursor;
    }

    // add the cursor to the adapter
    adapter.swapCursor(cursor);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
    adapter.swapCursor(null);
}
}

      

CursorAdapter - AdapterContacts.java

public class AdapterContacts extends CursorAdapter {

// Constants
private static final String TAG = "AdapterContacts";
private static final boolean DEBUG = true;

// Variables
private TextView mName;
private ContentResolver mContent;

/**
 * Constructor
 */
public AdapterContacts(Context context, Cursor c, int flags) {
    super(context, c, flags);
    mContent = context.getContentResolver();
}

/**
 * Overrides
 */

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
    // Inflate the views that create each row of the dropdown list
    final LayoutInflater inflater = LayoutInflater.from(context);
    final LinearLayout ret = new LinearLayout(context);
    ret.setOrientation(LinearLayout.VERTICAL);

    mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
    ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

    /*
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    mName.setText(cursor.getString(nameIdx));
    */

    return ret;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
    // Fill the dropdown row with data from the cursor
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    String name = cursor.getString(nameIdx);
    ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
}

@Override
public String convertToString(Cursor cursor) {
    // Convert the dropdown list entry that the user clicked on
    // into a string that will fill the AutoCompleteTextView
    int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    return cursor.getString(nameCol);
}

@Override
public void changeCursor(Cursor newCursor) {
    // Because a LoaderManager is used to initialize the originalCursor
    // changeCursor (which closes cursors be default when they're released)
    // is overridden to use swapCursor (which doesn't close cursors). 
    Cursor oldCursor = swapCursor(newCursor);

    // Any swapped out cursors that are not the original cursor must 
    // then be closed manually.
    if (oldCursor != Home.originalCursor) {
        oldCursor.close();
    }
}

@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
    // If their is a constraint, generate and return a new cursor
    if (constraint != null) {
        // I'd love to use a LoaderManager here too,
        // but haven't quite figured out the best way.
        if (DEBUG) Log.i(TAG, "Constraint is not null: " + constraint.toString());
        Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, constraint.toString());
        return mContent.query(uri, null, null, null, null);
    }

    // If no constraint, return the originalCursor
    if (DEBUG) Log.i(TAG, "Constraint is null");
    return Home.originalCursor;
}
}

      

Error message

03-16 10:39:34.839: E/AndroidRuntime(22097): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.myapp.basic/com.myapp.basic.Home}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1956)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1981)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3351)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.access$700(ActivityThread.java:123)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1151)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Looper.loop(Looper.java:137)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.main(ActivityThread.java:4424)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invokeNative(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invoke(Method.java:511)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at dalvik.system.NativeStart.main(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097): Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.getColumnNames(BulkCursorToCursorAdaptor.java:170)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndex(AbstractCursor.java:248)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:265)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.CursorWrapper.getColumnIndexOrThrow(CursorWrapper.java:78)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.widget.CursorAdapter.swapCursor(CursorAdapter.java:338)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:70)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:1)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:438)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.finishRetain(LoaderManager.java:309)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl.finishRetain(LoaderManager.java:765)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.Activity.performStart(Activity.java:4485)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1929)
03-16 10:39:34.839: E/AndroidRuntime(22097):    ... 12 more

      

Warning message - when the Activity is configured to manage the orientation changes themselves

03-16 10:47:50.804: W/Filter(22739): An exception occured during performFiltering()!
03-16 10:47:50.804: W/Filter(22739): android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:81)
03-16 10:47:50.804: W/Filter(22739):    at android.database.CursorWrapper.getCount(CursorWrapper.java:57)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.CursorFilter.performFiltering(CursorFilter.java:53)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.Filter$RequestHandler.handleMessage(Filter.java:234)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Looper.loop(Looper.java:137)
03-16 10:47:50.804: W/Filter(22739):    at android.os.HandlerThread.run(HandlerThread.java:60)

      

+3


source to share


2 answers


I found a solution (solution) for my problem in two parts:

  • I couldn't keep generating cursors manually in my CursorAdapter. I had to start using the bootloader manager
  • I didn't have to hold onto any cursors.

Note. For anyone else next, I still get a small error when running this code, but it is not fatal and does not seem to be related to cursors, so I am not covering it here.

Creating cursors in CursorAdapter

The big complication is that the runQueryOnBackgroundThread

CursorAdapter method requires a cursor to return. When using the LoaderManager, you don't get your hands on the cursor until the asynchronous callback, and it has its downsides:

  • You cannot generate and retrieve a cursor from a method runQueryOnBackgroundThread

    .
  • You cannot generate the cursor earlier because this runQueryOnBackgroundThread

    is the first method with a new filtering constraint.
  • The method runQueryOnBackgroundThread

    moves the cursor to the method changeCursor

    that closes the cursors that have changed (something we don't do when working with LoaderManager / CursorLoader), so we don't want to follow this workflow anyway.

By default, the method runQueryOnBackgroundThread

for CursorAdapter simply calls the method runQuery

for CursorAdapter's FilterQueryProvider, if defined. I decided to define the FilterQueryProvider instead of overriding the method runQueryOnBackgroundThread

for several reasons:

  • The FilterQueryProvider can be defined from the Activity that instantiated the CursorAdapter, and using the LoaderManager from the Activity was easier by using it from within the CursorAdapter.
  • I prefer the code to do what it was supposed to do, if at all possible.

Note. The method runQuery

still requires the cursor to be returned, so we cannot work around this issue.

I decided to create a dummy cursor in my FilterQueryProvider method runQuery

. Then, since this dummy cursor will be passed to the CursorAdapter method changeCursor

, I tried changeCursor

to just close every cursor it passed.



The method runQuery

also initiates an asynchronous call to LoaderManager, which enables the filtering constraint. The LoaderManager callbacks then take care of the swapping in the newly created cursors.

Note. Ideally, I assume you can override the calling function runQueryOnBackgroundThread

and force it to make an asynchronous call to LoaderManager, but I couldn't figure out what that was.

Hanging to cursors

I was trying to differentiate between unfiltered and filtered cursors to an unfiltered cursor that could be used when the filtering limit was zero. After reading Android 3.0 - what are the benefits of using LoaderManager instances exactly? the first time, I realized that the accepted answer used the same CursorLoader to generate all cursors.

Instead of trying to hover on the original unfiltered cursor, I figured I would just create a new unfiltered cursor whenever I needed it. The onCreateLoader

LoaderManager callback has gotten a bit more complex (but more like the examples I've seen) and the callback has gotten a onLoadFinished

lot simpler (like the examples I've seen).

Source

Activity - home.java

public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {

// Constants
private static final String TAG = "Home";
private static final boolean DEBUG = true;
public static final int LOADER_CONTACTS_CURSOR = 1;

// Variables
private AdapterContacts adapter;



/**
 * Overrides
 */

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set the view
    setContentView(R.layout.home);

    // Initialize CursorAdapter
    adapter = new AdapterContacts(this, null, 0);
    final LoaderManager.LoaderCallbacks<Cursor> iFace = this;
    adapter.setFilterQueryProvider(new FilterQueryProvider() {
        public Cursor runQuery(CharSequence constraint) {
            if (constraint != null) {
                Bundle bundle = new Bundle();
                bundle.putCharSequence("constraint", constraint);
                getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, bundle, iFace);
            } else {
                getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, null, iFace);
            }
            return getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
        }
    });

    // Attach CursorAdapter to AutoCompleteTextView field
    AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
    field.setAdapter(adapter);

    // Initialize Cursor using LoaderManagers
    LoaderManager.enableDebugLogging(true);
    getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
}

@Override
public void onDestroy() {
    if (DEBUG) Log.i(TAG, "Destroying activity");
    super.onDestroy();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
    Uri baseUri;

    if (args != null) {
        CharSequence constraint = args.getCharSequence("constraint");
        if (DEBUG) Log.i(TAG, "Constraint: " + constraint.toString());
        baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(constraint.toString()));
    } else {
        if (DEBUG) Log.i(TAG, "No Constraint");
        baseUri = ContactsContract.Contacts.CONTENT_URI;
    }
    return new CursorLoader(this, baseUri, null, null, null, null);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
    adapter.swapCursor(cursor);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
    adapter.swapCursor(null);
}
}

      

CursorAdapter - AdapterContacts.java

public class AdapterContacts extends CursorAdapter {

// Constants
private static final String TAG = "AdapterContacts";
private static final boolean DEBUG = true;

// Variables
private TextView mName;

/**
 * Constructor
 */
public AdapterContacts(Context context, Cursor c, int flags) {
    super(context, c, flags);
}

/**
 * Overrides
 */

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
    // Inflate the views that create each row of the dropdown list
    final LayoutInflater inflater = LayoutInflater.from(context);
    final LinearLayout ret = new LinearLayout(context);
    ret.setOrientation(LinearLayout.VERTICAL);

    mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
    ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

    /*
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    mName.setText(cursor.getString(nameIdx));
    */

    return ret;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
    // Fill the dropdown row with data from the cursor
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    String name = cursor.getString(nameIdx);
    ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
}

@Override
public String convertToString(Cursor cursor) {
    // Convert the dropdown list entry that the user clicked on
    // into a string that will fill the AutoCompleteTextView
    int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    return cursor.getString(nameCol);
}

@Override
public void changeCursor(Cursor newCursor) {
    newCursor.close();
}
}

      

+6


source


If you are using Loader

to control Adapter

Cursor

, you need to avoid using adapter.filter()

at all costs. Because the adapter filter()

pending returns Cursor

, which is not possible because the loaders are asynchronous and run on a background thread.

How to replace with adapter.filter()

bootloaders:

  • Inside the onSelect / Item / TextInput listener, store the selected / input values ​​in a class variable.

  • Replace all calls with myAdapter.filter()

    by myLoadManager.restartLoader(...)

    .

  • In your method onCreateLoader()

    , using the stored selected / entered values ​​that you now got from step 1, dynamically generate your sql / cursor query and run it.

This is how the sequence of events will occur:



  • The user selects the unwinder and selects "USA".

  • You store "US" in a class variable and then call myLoadManager.restartLoader(...)

    .

  • The download manager destroys the previous download manager and creates a new caller onCreateLoader()

    that has your generated request code.

  • The new loader runs an updated request and notifies your adapter to update and redraw it.

Why do you want to use Loaders

?

  • Free / lightweight asynchronous management of non-blocking UI updates based on slow data. Much easier than AsyncTasks

    .

  • Free memory management of your cursors. Will automatically clear / close Cursors

    when re-requesting or closing an activity.

0


source







All Articles