Showing posts with label debugging. Show all posts
Showing posts with label debugging. Show all posts

Thursday, July 5, 2012

Never start or shutdown a service in android.test.ServiceTestCase.setUp() or tearDown()

Setting up your service setUp() or stopping it in tearDown() will break your test-case. Most probably a NullPointerException will be thrown somewhere. Both errors happen independently, but have in essence the same cause: the internal state keeping of ServiceTestCase.

The class Service does not keep track of the service life-cycle state, i.e. whether the service has been created, started, or is about to be destroyed. That is why the ServiceTestCase defines a number of state variables for tracking the state: mServiceAttached, mServiceCreated, mServiceStarted, and mServiceBound. The problem lies with mServiceCreated. Let's check it out in detail.

Calling startService() in setUp()

Starting the service in setUp() might cause an error during testServiceTestCaseSetUpProperly()—and for that matter, during any other of your own tests.

In my case it was only testServiceTestCaseSetUpProperly() that failed with an error. It is the sole test-case defined by ServiceTestCase. Its purpose is to detect improperly configured tests. I does only one thing: calling setupService().

My service was still very rudimentary, resembling the local service example from the API docs of class Service. The test failed during the tearDown(). Per default, tearDown() shuts down the service. This includes invoking onDestroy() on the service. There occurred a NullPointerException. The implementation of my onDestroy() method is as follows:

@Override
public void onDestroy() {
    // Cancel the persistent notification.
    mNM.cancel(NOTIFICATION);
    // Tell the user we stopped.
    Toast.makeText(this, R.string.music_player_stopped, Toast.LENGTH_SHORT).show();
}

Surprisingly the variable mNM (of class NotificationManager) was null, though it was initialized properly in onCreate():

@Override
public void onCreate() {
    mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
    // Display a notification about us starting.  We put an icon in the status bar.
    showNotification();
}

So, when did it went null again? Well, never! Due to calling startService(…) in setUp() it just never had been initialized! It was initialized once, all right. And that was during startService(…). But right after that, testServiceTestCaseSetUpProperly() called setupService(), which will create a new service instance, but will not start it—i.e. onCreate() will not be invoked upon it. So my precious—and initialized—service instance was silently exchanged against an uninitialized one. That alone would not be fatal, were not the state variable ServiceTestCase.mServiceCreated still set to true, thus signaling that onCreate() had been invoked on the service. And because of that, tearDown() will happily invoke onDestroy() upon a service instance whose onCreate() method never had been called. Smells a bit like a bug to me…

Calling shutdownService() in tearDown()

In the case of the shutdown the cause of the error is easy to pinpoint: ServiceTestCase.tearDown() also shuts down the service—and the API docs clearly say so. It is understandable that a second attempt will fail, finding its underlying service gone. Or is it? The error happens in ServiceTestCase.shutdownService(), in line 10 of the following listing:

protected void shutdownService() {
    if (mServiceStarted) {
        mService.stopSelf();
        mServiceStarted = false;
    } else if (mServiceBound) {
        mService.onUnbind(mServiceIntent);
        mServiceBound = false;
    }
    if (mServiceCreated) {
        mService.onDestroy();
    } 
}

The call to ServiceTestCase.tearDown() has just executed this very same code once and afterwards nullified the service instance. However, mServiceCreated is (again) still true, although the service is already destroyed. In fact, this status variable is never re-set to false anywhere in ServiceTestCase. Thus onDestoy() will be invoked on a null reference.

Conclusion

The moral is, never call startService(…) in setUp() and never shutdownService() in tearDown(). It would be nice however, if the official documentation would impress a little more upon that. Also, a little less unforgiving implementation seems possible. Basically that would mean to just re-set mServiceCreated to false in two or so places.

Thursday, May 31, 2012

Eclipse ADT: Conditional Breakpoint Error

When I tried to understand the way Android's RelativeLayout measures and re-sizes the views it contains, I had do debug the Android SDK sources. Eclipse makes that quiet easy. All you have to do is attach the sources of the SDK you are using to the «android.jar» in your project's preferences.
Stepping through the source and setting breakpoints worked flawlessly. But when I wanted to set a conditional breakpoint (because I wanted to stop execution when RelativeLayout.onMeasure measured a specific sub-view), Eclipse failed with an error:

Conditional Breakpoint Error

It reads: «Conditional Breakpoint Error. Conditional breakpoint has compilation error(s). Reason: Unable to compile conditional breakpoint - missing Java project context.» Also, evaluating and displaying Java expressions would not work and gave the following error:

Error Evaluating

It reads: «Error Evaluating. Unable to evaluate the selected expression: To perform an evaluation, an expression must be compiled in the context of a Java project's build path. The current execution context is not associated with a Java project in the workspace.» Evaluated expressions come in handy, for instance, when picking the MeasureSpec of a View apart, which is a bit-field composed of a length and a mode.

Searching the internet for a solution, I only unearthed the rather vague suggestion to—surprise!—create a Java project for the Android library sources. I was not really fond of that idea, subdued by the size of the library. Not without reason, as I saw later. So I asked for a solution over at stackoverflow.com, but to no avail. I suppose the problem is to specific. When nothing came out of it, I had to swallow the pill and create a project for the sources of the Android library. And guess what? It fixed the problem!

Step by step do the following. First create a new Java project. A wizard dialog will pop up. Name the project as you like, «Android library sources» seems appropriate. On the next page, link an additional source and point it to the appropriate source directory in the Android SDK. Eclipse will now ask for «Inclusion and Exclusion Patterns». Exclude everything by adding the asterisk «*» to the exclusion pattern. This both surprising and important. Surprising because Eclipse does not need to do anything with the sources (like compiling) for conditional breakpoints and the like to work. And it is important, because otherwise your log will overflow with warnings, and error and info messages. Finish the dialog.
Now you need to attach the source code to the «android.jar» library of your project. Open your project's properties. You will find the library under «Java Build Path» on tab «Libraries» as show by the following screen shot:

Attaching the source code to the Android library

I my original configuration I had attached the source from an «External Folder» instead of a workspace, which only worked partway.

And that's it!