Wednesday, August 3, 2011

ListView / Async thread issue workaround

Sometimes, you hit a bug which you immediately know is going to be a major time-sink. I call them "iceburg" errors, because 90% of what you need to know is hidden underneath the surface of the your code in inside someone else's, and they are immovable.

The way to handle them is to look at it as a learning opportunity. If you didn't like programming, you wouldn't be doing this anyway, right? Try to understand that one day, this too shall pass. That the key to life is enjoying the process of learning. To open your mind to the nourishing flow of knowledge, and become receptive. Once you've taken this step back, and allowed yourself to take the time enjoy the process of discovery, you will achieve *acceptance* that this is just going to take the time it takes - you will be in a better frame of mind to explore and understand the mysterious and fascinating issue you've uncovered. It's all the better if your boss buys into this :) Then turn on some nice, calming background music - I have a flamenco station on Pandora that works perfectly for this - and dive in.

Having achieved this more pleasant, enlightened and receptive state of mind, we are now in a better position to take a look at this zero-children on ListView error we've run into, where the onClickItem method receives an empty child list - every once in a while.

Here are the parameters of the problem.

First, the activity is a ListActivity:

public class QuestionActivity extends ListActivity

which means it has some methods built into it so you don't have make your own.

Secondly, it uses a "CustomFontStringArrayAdapter", as follows:

customFontStringArrayAdapter = new CustomFontStringArrayAdapter(this,
R.layout.simple_list_item_single_choice_custom, mChoiceArrayList,
appState.getTypeface()) {
};


What's this CustomFontStringArrayAdapter? It's a class we created that extends ArrayAdapter. It uses a custom font, as you might imagine.

Tracing back through the object hierarchy, we find it all starts with the View class:

This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc.). The ViewGroup subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties.

This is extended by a ViewGroup:

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.


This is in turn extended by an AdapterView:

An AdapterView is a view whose children are determined by an Adapter.

See ListView, GridView, Spinner and Gallery for commonly used subclasses of AdapterView.


The base class of an AdapterView is the abstract class BaseAdapter:
BaseAdapter:

Common base class of common implementation for an Adapter that can be used in both ListView (by implementing the specialized ListAdapter interface} and Spinner (by implementing the specialized SpinnerAdapter interface.

// So, what's an Adapter?

An Adapter object acts as a bridge between an AdapterView and the underlying data for that view. The Adapter provides access to the data items. The Adapter is also responsible for making a View for each item in the data set.

// So, the Adapter ties together the AdapterView the view and the data, which is where
// the Array Adapter comes into play.

Base Adapter is extended by ArrayAdapter:

(ArrayAdapter is) a concrete BaseAdapter that is backed by an array of arbitrary objects. By default this class expects that the provided resource id references a single TextView. If you want to use a more complex layout, use the constructors that also takes a field id. That field id should reference a TextView in the larger layout resource.

However the TextView is referenced, it will be filled with the toString() of each object in the array. You can add lists or arrays of custom objects. Override the toString() method of your objects to determine what text will be displayed for the item in the list.

To use something other than TextViews for the array display, for instance, ImageViews, or to have some of data besides toString() results fill the views, override getView(int, View, ViewGroup) to return the type of view you want.


Ok, so all this CustomFontStringArrayAdapter is for is to tie together the "simple_list_item_single_choice_custom" with the entries in "mChoiceArrayList" - with a custom font.

Ok, let's continue:

We've got some code to fill up the mChoiceArrayList:

int randSlot = Utils.genRandomNumber(0, 4);

appState.mAnswerArray[randSlot] = mQuestion.hiragana;

mChoiceArrayList.clear();
mChoiceArrayList.addAll(Arrays.asList(appState.mAnswerArray));

The actual declaration of mChoiceArrayList is this:

ArrayList mChoiceArrayList = new ArrayList();

and it's only done once, after the class declaration.

Then we do this:

// attach the adapter to the ListView
setListAdapter(customFontStringArrayAdapter);

// set ListView's choice mode to single
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);

So, the setListAdapter comes from the ListActivity. Let's examine the docs:

(A ListActivity is) an activity that displays a list of items by binding to a data source such as an array or Cursor, and exposes event handlers when the user selects an item.

// such as onListItemClick

ListActivity hosts a ListView object that can be bound to different data sources, typically either an array or a Cursor holding query results.

// Yes, and our ListView is described in our layout.
// The View for each item and the backing data are tied together in our
// CustomFontStringArrayAdapter class. These are in turn applied
// to the ListView in this statement:

// attach the adapter to the ListView
setListAdapter(customFontStringArrayAdapter);



Binding, screen layout, and row layout are discussed in the following sections.

Screen Layout

ListActivity has a default layout that consists of a single, full-screen list in the center of the screen. However, if you desire, you can customize the screen layout by setting your own view layout with setContentView() in onCreate().

// we do this:
/* expand layout */
setContentView(R.layout.question);


To do this, your own view MUST contain a ListView object with the id "@android:id/list" (or list if it's in code)

// Here's our layout:

android:cacheColorHint="#00000000" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:textColor="@color/text_color"
android:layout_marginLeft="6dp" android:divider="#333000"
android:dividerHeight=".5dp" android:focusable="false">



Optionally, your custom view can contain another view object of any type to display when the list view is empty. This "empty list" notifier must have an id "android:id/empty". Note that when an empty view is present, the list view will be hidden when there is no data to display.

// We never have this, or shouldn't. Our debugging has shown that the array we're
// using is not empty when we're getting the empty error.

The following code demonstrates an (ugly) custom screen layout. It has a list with a green background, and an alternate red "no data" message.


android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">

android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00FF00"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>

android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0000"
android:text="No data"/>


// What's that drawSelectorOnTop?

Row Layout

You can specify the layout of individual rows in the list. You do this by specifying a layout resource in the ListAdapter object hosted by the activity (the ListAdapter binds the ListView to the data; more on this later).

A ListAdapter constructor takes a parameter that specifies a layout resource for each row. It also has two additional parameters that let you specify which data field to associate with which object in the row layout resource. These two parameters are typically parallel arrays.

Android provides some standard row layout resources. These are in the R.layout class, and have names such as simple_list_item_1, simple_list_item_2, and two_line_list_item. The following layout XML is the source for the resource two_line_list_item, which displays two data fields,one above the other, for each list row.


android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

android:textSize="16sp"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

android:textSize="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>


// We're actually using the following:


style="@style/CodeFont"
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="35sp"
android:gravity="center_vertical"
android:checkMark="?android:attr/listChoiceIndicatorSingle"
android:paddingLeft="20dip"
android:paddingTop="10dip"
android:paddingBottom="10dip"
android:background="#AAEBD5A8"

/>


You must identify the data bound to each TextView object in this layout. The syntax for this is discussed in the next section.

Binding to Data

You bind the ListActivity's ListView object to data using a class that implements the ListAdapter interface. Android provides two standard list adapters: SimpleAdapter for static data (Maps), and SimpleCursorAdapter for Cursor query results.

// We're using an ArrayListAdapter

The following code from a custom ListActivity demonstrates querying the Contacts provider for all contacts, then binding the Name and Company fields to a two line row layout in the activity's ListView.

public class MyListAdapter extends ListActivity {

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

// We'll define a custom screen layout here (the one shown above), but
// typically, you could just use the standard ListActivity layout.
setContentView(R.layout.custom_list_activity_view);

// Query for all people contacts using the Contacts.People convenience class.
// Put a managed wrapper around the retrieved cursor so we don't have to worry about
// requerying or closing it as the activity changes state.
mCursor = this.getContentResolver().query(People.CONTENT_URI, null, null, null, null);
startManagingCursor(mCursor);

// Now create a new list adapter bound to the cursor.
// SimpleListAdapter is designed for binding to a Cursor.
ListAdapter adapter = new SimpleCursorAdapter(
this, // Context.
android.R.layout.two_line_list_item, // Specify the row template to use (here, two columns bound to the two retrieved cursor
rows).
mCursor, // Pass in the cursor to bind to.
new String[] {People.NAME, People.COMPANY}, // Array of cursor columns to bind to.
new int[] {android.R.id.text1, android.R.id.text2}); // Parallel array of which template objects to bind to those columns.

// Bind to our new adapter.
setListAdapter(adapter);
}
}


// Ok, interesting, this works a little bit differently than the ArrayAdapter, which is:

A concrete BaseAdapter that is backed by an array of arbitrary objects. By default this class expects that the provided resource id references a single TextView. If you want to use a more complex layout, use the constructors that also takes a field id. That field id should reference a TextView in the larger layout resource.

// that's it, it expects the resource id references a single TextView.

Ok. So for, so good. It's a good review, and enhances and refreshes our knowledge of ListActivity. There more to review:

See Also

setListAdapter(ListAdapter)
ListView


public void setListAdapter (ListAdapter adapter)

Since: API Level 1
Provide the cursor for the list view.

What's a ListAdapter? It's an interface...

Class Overview

Extended Adapter that is the bridge between a ListView and the data that backs the list. Frequently that data comes from a Cursor, but that is not required. The ListView can display any data provided that it is wrapped in a ListAdapter.

Known Indirect Subclasses
ArrayAdapter, BaseAdapter, CursorAdapter, HeaderViewListAdapter, ResourceCursorAdapter, SimpleAdapter, SimpleCursorAdapter, WrapperListAdapter


So, the ArrayAdapter is an inderect subclass, which is why it works in the setListAdapter.


I'm just wondering if I do a "getListView" in the onItemClick, if it will get the List View? Or if I store it as a member variable? It might even solve this reversed order of items problem, where the position. Well, first, let's add some debugging statements on the onListItemClick:

if (childCount == 0) {
ListView myTempListView = QuestionActivity.this.getListView();

int tempChildCount = myTempListView.getChildCount();

Log.d(TAG, "*** ListView is empty ***");
Log.d(TAG, "tempChildCount: " + tempChildCount);
Log.d(TAG, "position: " + position);
Log.d(TAG, "id: " + id);
if (null == listItemView){
Log.d(TAG, "id: " + id);
}

else {
Log.d(TAG, "listItemView: " + listItemView);
}


}


and also before the list setup:

Log.d(TAG, "*******************************************************");
Log.d(TAG, "appState.mAnswerArray: " + appState.mAnswerArray);
Log.d(TAG, "appState.mAnswerArray.length: " + appState.mAnswerArray.length);

Log.d(TAG, "mChoiceArrayList: " + mChoiceArrayList );
Log.d(TAG, "mChoiceArrayList.size(): " + mChoiceArrayList.size() );

Log.d(TAG, "listView before: " + listView);

Log.d(TAG, "listView before childCount: " + listView.getChildCount());

Log.d(TAG, "*******************************************************");

Ok, let's see if we can garner any insights from this.


There it is:

D/QuestionActivity(29556): listView before: android.widget.ListView@40617d28
D/QuestionActivity(29556): listView before childCount: 0
D/QuestionActivity(29556): *******************************************************
D/QuestionActivity(29556): *** ListView is empty ***
D/QuestionActivity(29556): tempChildCount: 0
D/QuestionActivity(29556): position: 3
D/QuestionActivity(29556): id: 3
D/QuestionActivity(29556): listItemView: android.widget.TextView@40646008
D

So, even in the setup, the child count shows as 0. Wait, it's zero even when it doesn't crash. So, before the click happens, is it always zero? No, actually, the countdown timer also uses it. And int that case, it's not zero.

What if I try it in browse mode? That doesn't run a separate thread to update the database.

Hmmm...no crash.

What if I disable the method call that triggers the the database update?

Hmmm..again no crash. I'm beginning to strongly suspect it's something to do with the AsyncTask. Threading.

Let's take a look at threading in Android. Here's a nice article:

http://developer.android.com/resources/articles/painless-threading.html

Painless Threading

This article discusses the threading model used by Android applications and how applications can ensure best UI performance by spawning worker threads to handle long-running operations, rather than handling them in the main thread.


The article also explains the API that your application can use to interact with Android UI toolkit components running on the main thread and spawn managed worker threads.

The UI thread

When an application is launched, the system creates a thread called "main" for the application. The main thread, also called the UI thread, is very important because it is in charge of dispatching the events to the appropriate widgets, including drawing events.


It is also the thread where your application interacts with running components of the Android UI toolkit.

For instance, if you touch the a button on screen, the UI thread dispatches the touch event to the widget, which in turn sets its pressed state and posts an invalidate request to the event queue.

// Ok, you touch a button - the UI Thread receives it and sends it to the
// widget instance. The widget handles it, and must at the point
// eliminate the original request with an invalidate request or something.

The UI thread dequeues the request and notifies the widget to redraw itself.

// right

This single-thread model can yield poor performance unless your application is implemented properly. Specifically, if everything is happening in a single thread, performing long operations such as network access or database queries on the UI thread will block the whole user interface.

No event can be dispatched, including drawing events, while the long operation is underway. From the user's perspective, the application appears hung. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous "application not responding" (ANR) dialog.

// Ah, so that's the time frame. Interesting.

If you want to see how bad this can look, write a simple application with a button that invokes Thread.sleep(2000) in its OnClickListener.

// I wonder if I did a Thread.sleep on the database update if it would make
// any difference?

The button will remain in its pressed state for about 2 seconds before going back to its normal state. When this happens, it is very easy for the user to perceive the application as slow.

To summarize, it's vital to the responsiveness of your application's UI to keep the UI thread unblocked. If you have long operations to perform, you should make sure to do them in extra threads (background or worker threads).

Here's an example of a click listener downloading an image over the network and displaying it in an ImageView:

public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork();
mImageView.setImageBitmap(b);
}
}).start();
}
At first, this code seems to be a good solution to your problem, as it does not block the UI thread. Unfortunately, it violates the single-threaded model for the UI: the Android UI toolkit is not thread-safe and must always be manipulated on the UI thread.

// Ok, can't change a UI widget from the UI thread

In this piece of code above, the ImageView is manipulated on a worker thread, which can cause really weird problems. Tracking down and fixing such bugs can be difficult and time-consuming.

Android offers several ways to access the UI thread from other threads. You may already be familiar with some of them but here is a comprehensive list:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler

You can use any of these classes and methods to correct the previous code example:

public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
final Bitmap b = loadImageFromNetwork();
mImageView.post(new Runnable() {
public void run() {
mImageView.setImageBitmap(b);
}
});
}
}).start();
}

Unfortunately, these classes and methods could also tend to make your code more complicated and more difficult to read. It becomes even worse when your implement complex operations that require frequent UI updates.



To remedy this problem, Android 1.5 and later platforms offer a utility class called AsyncTask, that simplifies the creation of long-running tasks that need to communicate with the user interface.

An AsyncTask equivalent is also available for applications that will run on Android 1.0 and 1.1. The name of the class is UserTask. It offers the exact same API and all you have to do is copy its source code in your application.

The goal of AsyncTask is to take care of thread management for you. Our previous example can easily be rewritten with AsyncTask:

public void onClick(View v) {
new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask {
protected Bitmap doInBackground(String... urls) {
return loadImageFromNetwork(urls[0]);
}

protected void onPostExecute(Bitmap result) {
mImageView.setImageBitmap(result);
}
}
As you can see, AsyncTask must be used by subclassing it. It is also very important to remember that an AsyncTask instance has to be created on the UI thread and can be executed only once. You can read the AsyncTask documentation for a full understanding on how to use this class, but here is a quick overview of how it works:

You can specify the type, using generics, of the parameters, the progress values and the final value of the task
The method doInBackground() executes automatically on a worker thread
onPreExecute(), onPostExecute() and onProgressUpdate() are all invoked on the UI thread
The value returned by doInBackground() is sent to onPostExecute()
You can call publishProgress() at anytime in doInBackground() to execute onProgressUpdate() on the UI thread
You can cancel the task at any time, from any thread
In addition to the official documentation, you can read several complex examples in the source code of Shelves (ShelvesActivity.java and AddBookActivity.java) and Photostream (LoginActivity.java, PhotostreamActivity.java and ViewPhotoActivity.java). We highly recommend reading the source code of Shelves to see how to persist tasks across configuration changes and how to cancel them properly when the activity is destroyed.

Regardless of whether or not you use AsyncTask, always remember these two rules about the single thread model:

Do not block the UI thread, and
Make sure that you access the Android UI toolkit only on the UI thread.
AsyncTask just makes it easier to do both of these things.


Ok, we see the UI Thread is not thread safe. Still, there's nothing to suggest the ListActivity would be getting tampered with somehow. If anything, you'd expect the database update to experience potential issues.

Let's take a look at ListView's source to see if we can glean any info from that.

http://grepcode.com/snapshot/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/

Search listview.

Wait, let's check ListActivity.

Ok, it has a descripton the same as in the docs.

≈This field should be made private, so it is hidden from the SDK.
184
185 protected ListView mList;
186
187 private Handler mHandler = new Handler();
188 private boolean mFinishedStart = false;
189
190 private Runnable mRequestFocus = new Runnable() {
191 public void run() {
192 mList.focusableViewAvailable(mList);
193 }
194 };


Ok, there's the infamous ListView member, mList.



Ensures the list view has been created before Activity restores all of the view states.
See also:
Activity.onRestoreInstanceState(android.os.Bundle)
215
216 @Override
217 protected void onRestoreInstanceState(Bundle state) {
218 ensureList();
219 super.onRestoreInstanceState(state);
220 }

Hmm...well, too bad the ensure list isn't working. Or it's not effective against the prolbem I'm having.

See also:
Activity.onDestroy()
224
225 @Override
226 protected void onDestroy() {
227 mHandler.removeCallbacks(mRequestFocus);
228 super.onDestroy();
229 }

Somehow you mut be able to add callback to mHandler.


Updates the screen state (current list and other views) when the content changes.
See also:
Activity.onContentChanged()
236
237 @Override
238 public void onContentChanged() {
239 super.onContentChanged();
240 View emptyView = findViewById(com.android.internal.R.id.empty);

// grabbing it from the layout
241 mList = (ListView)findViewById(com.android.internal.R.id.list);


242 if (mList == null) {
243 throw new RuntimeException(
244 "Your content must have a ListView whose id attribute is " +
245 "'android.R.id.list'");
246 }
247 if (emptyView != null) {
248 mList.setEmptyView(emptyView);
249 }
250 mList.setOnItemClickListener(mOnClickListener);
251 if (mFinishedStart) {
252 setListAdapter(mAdapter);
253 }
254 mHandler.post(mRequestFocus);
255 mFinishedStart = true;
256 }


Provide the cursor for the list view.
260
261 public void setListAdapter(ListAdapter adapter) {
262 synchronized (this) {
263 ensureList();
264 mAdapter = adapter;
265 mList.setAdapter(adapter);
266 }
267 }

Ok, it keeps a reference to the adapter and sets it to the mList.


Set the currently selected list item to the specified position with the adapter's data
Parameters:
position
274
275 public void setSelection(int position) {
276 mList.setSelection(position);
277 }

It's a call through.

Get the position of the currently selected list item.
281
282 public int getSelectedItemPosition() {
283 return mList.getSelectedItemPosition();
284 }


Alsa Call-through.


Get the cursor row ID of the currently selected list item.
288
289 public long getSelectedItemId() {
290 return mList.getSelectedItemId();
291 }


Call through to get the id of the selected item (long variable)

Get the activity's list view widget.
295
296 public ListView getListView() {
297 ensureList();
298 return mList;
299 }

Sure.

Get the ListAdapter associated with this activity's ListView.
303
304 public ListAdapter getListAdapter() {
305 return mAdapter;
306 }
307

Ok

308 private void ensureList() {
309 if (mList != null) {
310 return;
311 }
312 setContentView(com.android.internal.R.layout.list_content);
313
314 }
315
316

Ok, this is just making sure the mList variable gets set.



316 private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
317 public void onItemClick(AdapterView parent, View v, int position, long id)
318 {
319 onListItemClick((ListView)parent, v, position, id);
320 }
321 };
322}


Ok, this is how the onListItemClick gets called. This is actually a variable declaration. I'm not completely sure how it - it seems like a delegation to the onListItemClick, that's sure. And the AdapterView is the parent, which turns out to be the ListView. Somehow it gets connected to the ListView.


How about ListView..wow - this is a huge class. Almost 4k lines.

Let's look at the api...

I'm pretty sure I can guarantee that the user can't crash it by setting the list view to disabled when the answer is displaying. There's a next button. But, I just find it very convenient to hit the answer selection twice instead of constantly switching back and forth to the "next" button. I'd prefer the occasional crash to giving that up - I think.

What would be good is if could even exit the onItemClick in that case. Just print an error and move on. At least it wouldn't be a crash.

This as an error that happens only if you pound on the display, say 50 times in a row. Most people aren't going to do that, but just in case, we'll move on. There will be one less on the total, but if you're going that fast, you really aren't thinking about your answers and most likely will have gotten others wrong, so you're level won't be advanced. Only pure luck would advance a level, and the level would have to be very small. Actually, since it won't add to correct answers, and the total questions doesn't change, it will essentially be classified as a wrong answer. Except it won't update the SRS table, which in this case has virtually the same impact. Unless they manually advance the level. Yeah, I'm thinking this is good. It will prevent a crash, just in case, and won't affect the game at all. And if they lose that question, they still get credit for all the correct questions.

Here's the workaround:

int childCount = listView.getChildCount();

// Workaround on bug raised by monkey exerciserd

if (childCount <= 0) {

ListView myTempListView = QuestionActivity.this.getListView();

int tempChildCount = myTempListView.getChildCount();

Log.d(TAG, "*** ListView was empty ***");
Log.d(TAG, "tempChildCount: " + tempChildCount);
Log.d(TAG, "position: " + position);
Log.d(TAG, "id: " + id);
if (null == listItemView) {
Log.d(TAG, "id: " + id);
}

else {
Log.d(TAG, "listItemView: " + listItemView);
}

try {
Thread.sleep(100); // Sleep for 1 sec
} catch (InterruptedException e) {
}

}

else {

// regular processing
AppState appState = (AppState) this.getApplication();

Well, this time I got through 57 questions without getting zero children per the log. Let set the questions available to the max.


Ok, this time it showed up:

/StagefrightPlayer( 68): setDataSource('/mnt/sdcard/data/.com.kanjisoft.jlpt5.full/aud5/5_461.mp3')
D/QuestionActivity(31284): **************************************************************
D/QuestionActivity(31284): appState.correctAnswers: 1
D/QuestionActivity(31284): appState.quizSequenceIndex : 11
D/QuestionActivity(31284): appState.quizSequence.size() : 542
D/QuestionActivity(31284): percent: : 9
D/QuestionActivity(31284): **************************************************************
D/QuestionActivity(31284): *******************************************************
D/QuestionActivity(31284): appState.mAnswerArray: [Ljava.lang.String;@4051acb0
D/QuestionActivity(31284): appState.mAnswerArray.length: 4
D/QuestionActivity(31284): mChoiceArrayList: [たぶん, はく, いろいろ, どうも]
D/QuestionActivity(31284): mChoiceArrayList.size(): 4
D/QuestionActivity(31284): listView before: android.widget.ListView@4061ae10
D/QuestionActivity(31284): listView before childCount: 0
D/QuestionActivity(31284): *******************************************************
D/QuestionActivity(31284): **** ListView was empty on click ***
D/QuestionActivity(31284): tempChildCount: 0
D/QuestionActivity(31284): position: 1
D/QuestionActivity(31284): id: 1
D/QuestionActivity(31284): listItemView: android.widget.TextView@40623c70
D/InitUtils(31284): appState.currentQuestion: 74
D/InitUtils(31284): appState.currentQuestion: 74
D/QuestionActivity(31284): **************************************************************
D/QuestionActivity(31284): appState.correctAnswers: 1
D/QuestionActivity(31284): appState.quizSequenceIndex : 12
D/QuestionActivity(31284): appState.quizSequence.size() : 542
D/QuestionActivity(31284): percent: : 8
D/QuestionActivity(31284): **************************************************************
D/QuestionActivity(31284): *******************************************************
D/QuestionActivity(31284): appState.mAnswerArray: [Ljava.lang.String;@4051acb0
D/QuestionActivity(31284): appState.mAnswerArray.length: 4
D/QuestionActivity(31284): mChoiceArrayList: [みぎ, はがき, みなみ, あける]
D/QuestionActivity(31284): mChoiceArrayList.size(): 4
D/QuestionActivity(31284): listView before: android.widget.ListView@4061ae10
D/QuestionActivity(31284): listView before childCount: 0
D/QuestionActivity(31284): *******************************************************
D/InitUtils(31284): appState.currentQuestion: 9
D/I


I actually could set the app sequence number back by one so it would repeat. I'm curious if it would do it on the same number.

Actually, if I tap really, really quickly, I can get the empty list to come up every few questions. Maybe if I put a very short Thread.sleep after setting the adapter..

No, even with a one-second sleep, I'm still able to generate error.

Well, just to be extra safe, we'll set the quiz sequence index back by one. If anyone actually ever should encounter the problem in the field, they will at least get the question repeated. Let's try it.

if (childCount <= 0) {

// Repeat the question
appState.quizSequenceIndex--;



/QuestionActivity(31845): appState.correctAnswers: 2
D/QuestionActivity(31845): appState.quizSequenceIndex : 10
D/QuestionActivity(31845): appState.quizSequence.size() : 517
D/QuestionActivity(31845): percent: : 20
D/QuestionActivity(31845): D/QuestionActivity(31845): *******************************************************
D/QuestionActivity(31845): appState.mAnswerArray: [Ljava.lang.String;@4051b4e8
D/QuestionActivity(31845): appState.mAnswerArray.length: 4
D/QuestionActivity(31845): mChoiceArrayList: [それ, やる, たいへん, あの]
D/QuestionActivity(31845): mChoiceArrayList.size(): 4
D/QuestionActivity(31845): listView before: android.widget.ListView@4061bfb8
D/QuestionActivity(31845): listView before childCount: 0
D/QuestionActivity(31845): *******************************************************
D/QuestionActivity(31845): *** ListView was empty on click **
D/QuestionActivity(31845): tempChildCount: 0
D/QuestionActivity(31845): position: 1
D/QuestionActivity(31845): id: 1
D/QuestionActivity(31845): listItemView: android.widget.TextView@40688e40
D/InitUtils(31845): appState.currentQuestion: 349
D/InitUtils(31845): appState.currentQuestion: 349
I/StagefrightPlayer( 68): setDataSource('/mnt/sdcard/data/.com.kanjisoft.jlpt5.full/aud5/5_349.mp3')
D/QuestionActivity(31845): **************************************************************
D/QuestionActivity(31845): appState.correctAnswers: 2
D/QuestionActivity(31845): appState.quizSequenceIndex : 10
D/QuestionActivity(31845): appState.quizSequence.size() : 517
D/QuestionActivity(31845): percent: : 18
D/QuestionActivity(31845): **************************************************************
D/QuestionActivity(31845): *******************************************************
D/QuestionActivity(31845): appState.mAnswerArray: [Ljava.lang.String;@4051b4e8
D/QuestionActivity(31845): appState.mAnswerArray.length: 4
D/QuestionActivity(31845): mChoiceArrayList: [ちょっと, あの, おもしろい, たいへん]
D/QuestionActivity(31845): mChoiceArrayList.size(): 4
D/QuestionActivity(31845): listView before: android.widget.ListView@4061bfb8
D/QuestionActivity(31845): listView before childCount: 0
D/QuestionActivity(31845): *******************************************************
D/I

There. As you see the choice array list contains the same answer, たいへん, albeit in different positions.

Ok, I'm going to clean up the comments, and leave one innocuous log just in case. It's a workaround.

No comments:

Post a Comment