Tuesday, May 31, 2011

AsyncTask download and updating the progress bar

I had a bit of trouble working out how to get the AsyncTask to update the progress bar on it's download; and examples were difficult to come by. As a result, I'm posting the AsyncTask subclass which will do the trick. Enjoy.


public class DownloadFilesTask extends AsyncTask {

private ProgressDialog dialog;
protected Context applicationContext;

// -- run intensive processes here
// -- notice that the datatype of the first param in the class
// definition
// -- matches the param passed to this method
// -- and that the datatype of the last param in the class definition
// -- matches the return type of this method
// -- The middle param matches the type of data passed to the process dialog.

@Override
protected String doInBackground(Void... params) {

downloadFromUrl();

// return Start.getTimeStampFromYahooService();
return "done";

}

@Override
protected void onPreExecute() {

this.dialog = new ProgressDialog(StartActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setProgress(0);
dialog.setMax(numberOfBytesInFileToDownload) // note you will have to know this in advance
dialog.setMessage("Loading...");
dialog.show();

}

// -- called from the publish progress
// -- notice that the datatype of the second param gets passed to this
// method
@Override
protected void onProgressUpdate(Integer... values) {

dialog.setProgress(values[0]);
}

// -- called if the cancel button is pressed
@Override
protected void onCancelled() {
}

// -- called as soon as doInBackground method completes
// -- notice that the third param gets passed to this method

@Override
protected void onPostExecute(String result) {
this.dialog.dismiss();
}


public void downloadFromUrl() { // this is the downloader method
Log.d(TAG, "enter download from url");
try {

String PATH = Environment.getExternalStorageDirectory()
+ "/data/com.example.myapp/";

Log.v(TAG, "PATH: " + PATH);
URL url = new URL(
"http://www.mysite/xdroid/mySoundFile.mp3");

HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();

File file = new File(PATH);
file.mkdirs();

String fileName = "mySoundFile.mp3";

File outputFile = new File(file, fileName);

long startTime = System.currentTimeMillis();
Log.d("DownloadTask", "download begining");
Log.d("DownloadTask", "download url:" + url);
Log.d("DownloadTask", "downloading file name:" + fileName);

/* Open a connection to that URL. */
FileOutputStream fos = new FileOutputStream(outputFile);

InputStream is = c.getInputStream();

byte[] buffer = new byte[1024];
int len1 = 0;
int cntr = 0;

while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
cntr++;
publishProgress(cntr * 1024);
}
fos.close();
is.close();

Log.d("DownloadTask",
"download ready in "
+ ((System.currentTimeMillis() - startTime) / 1000)
+ " sec");

} catch (IOException e) {
Log.d("ImageManager", "Error: " + e);
}
Log.d(TAG, "exit download from url");

} // download from url

}

Note that the publishProgress pass the number of bytes downloaded, which will be used to display that metric vs. total file size, and calculate a percent as well.

Monday, May 30, 2011

The download - step 2

Well, I'm ready to tackle step 2 of the download process. In recent post, I basically dissected the async task, because a huge download is something you are definitely going to want to run asynchronously. Today, I am shooting for actually going ahead and downloading that file.

So, where is that old download logic? Oh, yeah - it was in the "download" button on the StartActivity page. Good.

Ok, it's successfully calling the onPreExecute, which displays this:

this.dialog = ProgressDialog.show(applicationContext, "Calling", "Download Service...", true);

But, nothing cancels it, so it just kind of sits there.

That's not true, the download cancels it, but the publishProgress in the doInBackground doesn't seem to be doing anything. So, let's comment it out.

Ok, that seemed to work out ok. The dialog flashed by, but since the doInB... didn't do anything, it went straight to the cancel. Now let's take the progressDialog and put it into a loop. Let's try an experiment and comment back in the publishProgress inside of a loop.

Well, it does get to the download service display - but it just hangs on that.

Ok, I'm taking my eye off the ball. I need to focus on getting the download taken care of :

Here's a URL: http://www.helloandroid.com/tutorials/how-download-fileimage-url-your-device

First, let's add these permissions:

android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.permission.READ_PHONE_STATE


Ok, that's done. Now, the code.

Here's what seems a reasonable compromise between a couple of examples:


public void downloadFromUrl() { // this is the downloader method
try {

String PATH = Environment.getExternalStorageDirectory()
+ "/xdownload/";
Log.v(TAG, "PATH: " + PATH);
URL url = new URL(
"http://www.mysite.com/test/lib.mp3");

HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();

File file = new File(PATH);
file.mkdirs();

String fileName = "test.mp3";

File outputFile = new File(file, fileName);

long startTime = System.currentTimeMillis();
Log.d("ImageManager", "download begining");
Log.d("ImageManager", "download url:" + url);
Log.d("ImageManager", "downloaded file name:" + fileName);
/* Open a connection to that URL. */

FileOutputStream fos = new FileOutputStream(outputFile);

InputStream is = c.getInputStream();

byte[] buffer = new byte[1024];
int len1 = 0;
while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
}
fos.close();
is.close();

Log.d("ImageManager",
"download ready in"
+ ((System.currentTimeMillis() - startTime) / 1000)
+ " sec");

} catch (IOException e) {
Log.d("ImageManager", "Error: " + e);
}

}


Ok, not bad - I got this:


D/ImageManager( 6501): Error: java.io.FileNotFoundException: /mnt/sdcard/xdownload/test.mp3 (No such file or directory)

Well, it seems to be doing the correct commands for creating the data, but the files just not getting created. What's going wrong?

Ok, I got it to work by adding an external storage permission in the manifest. But, unfortunately, I still can't seem to get the progress dialog to work. It also keeps showing the file not found exception.

Here's the example from google:

protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
}
return totalSize;
}

protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}



So, the publishProgress looks like it's called with an int percentage of the # of files downloaded. Does it have some kind of mechanism that takes whatever's passed in, turns it into an object array? Oh, yeah, that's what the middle parameter in the Async Vector is for.

Ah, ok, the problem is although it's creating the file, it's dying on the file not found exception. So, it's never making the calls to display the dialog, the file's never getting written to.

Could it be it needs read permission, too?



Wait - it's writing to "lib.mp3". I've been creating test.mp3. Change the created file to lib.mp3 instead of the test.mp3.

Nope. Still no good. File not found. Wait - now it's the url file name it's not finding! Cool. I'm making progress.

Ah, right. The url I coded includes public_html - no need to specify that. The directory names begin after that. Fix that.

Cool - it found it alright - then went into a mad loop about resources or memory exceptions. I'm not sure if it was the file or the dialogue box. I suspect it was the dialog box because I was displaying for every thousand bytes. But, I'm not sure.

Did the file get written to?

Yes, indeed it did. It's just one mp3. The zip file will be much huger.

Well, let's get rid of the dialog, and see if it goes more smoothly....

Yep. Well, as I suspected, the download is going to be less of a problem than displaying the progress dialogue.

Maybe if I tried every thousand?

Ah, right, because I'm canceling right away, nothings showing up. No, it's just not getting called. I'm doing something wrong. Ok, what I'll do is change it back to a no parameters for now.

Nah; I'll just call here. I'll figure that part out tomorrow. Right now, let's watch Die Hard!

Saturday, May 28, 2011

Tackling the async download

My goal is to get done *very soon* -as in today - the part of my app that downloads the mp3 audio files and puts them on the SD card.

Google download - ah, there's another problem I need to look after - keeping them from showing up when you use astro or
something like that.

http://androinica.com/2009/08/how-to-hide-non-music-audio-files-from-appearing-in-android-media-players/

1. On your computer, open Notepad or any other text editor

2. Save the blank file as “.nomedia”
Make sure that the Save as type is set as “All Files” instead of “Text documents”

3. Copy that file to the folder on your SD card containing audio files you don’t want to show up.
(Example: If you want to block CoPilot sound files from coming up, place the .nomedia file in the sdcard/copilot folder)

4. Reboot your phone and the files should no longer be viewable

This same trick works for folders containing images or videos that you don’t want to appear in the Gallery app. The files will still be accessible from your original app or an explorer like Astro or Linda, but you’ll finally be able to stop hearing “Turn left” announcements after a great song.

Let's see if this works.

First, create the .notepad on say desktop.

Check.

How again do you copy a file from your desktop to a directory?

I think it must be adb push.

Open terminal.

Ok, I'm doing adb shell, and for some reason it's not given me a not authorized when I cd around anymore. Well, that's good.

Well, I copied it, but it doesn't work. I'm noticing that it says open it it in notepad and save the file as type All Files, but I'm using textmate on a mac. So, now what do I do?

google all types mac?


Ok, we'll get back to that.


Here's something the looks useful...

public void mp3load() {
URL url = new URL(url);
HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();

String PATH = Environment.getExternalStorageDirectory()
+ "/download/";
Log.v(LOG_TAG, "PATH: " + PATH);
File file = new File(PATH);
file.mkdirs();

String fileName = "test.mp3";


File outputFile = new File(file, fileName);
FileOutputStream fos = new FileOutputStream(outputFile);

InputStream is = c.getInputStream();

byte[] buffer = new byte[1024];
int len1 = 0;
while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
}
fos.close();
is.close();
}

I think it's a zip file, and all the files are in these individual files - where did I put it?

Ok, it was in a folder called "japanrelated/jlpt_named_folders".

Let's upload it. Ok, I need to reconnect the ftp. It's 1 113 MB file. That's nothing unless you have no SD card at all.

Ok. It's going, but not that fast. 57 KB = 400 kb /sec upload, approximately. Still, not bad for a coffee shop.

I think I will code it as an an async task. While I'm waiting I'll get the code for an async task.

For now, I'll just add a button to the start activity which the user will press to activate the download. I will do it automatically later.

There's some code from this link http://appfulcrum.com/?p=126

This should be a one step at a time thing. Going with TDD spirit, implement the smallest possible action.

Create a download button in StartActivity.xml.

Hmm, I actually never renamed the start activity layout from the previous quiz. Ok - rename.

Hmm...crash. I need to call initialize, I think, which I commented out yesterday.

Crash again. Why isn't the default value getting set? Risk a debug.

It's just sitting there waiting for the debugger - for some reason it won't attach.

Run, and It's crashing on line 61.

mJlptLevel.setText(new Integer(appState.jlptLevel).toString());

It can only be mjlptLevel or appstate.jlptLevel.

D/QuizStartActiity( 1491): >>>>>>>>>>>>>>> mJlptLevel: null

Ok, there it is. So...do I have to clean the project? Yup - that seemed to do it. Never make assumptions.

Ok, getting back to the "test" - add a download button to start layout.

Ok, finally, I've got the button going on. Now, let's add a Toast display.

It's something like onButtonClick. Well, let's work off the example.

Ok, that's done.

Now, the actual purpose of the button is to kick off an asynchronous activity.

It looks like there's a good example in the developer docs:

http://developer.android.com/reference/android/os/AsyncTask.html

private class DownloadFilesTask extends AsyncTask {
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
}
return totalSize;
}

protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}

protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}


Ok. It looks like this actually takes a list of urls. Given the odd file names, and the thousands of files, I'll just download one zip file and contend with that.

It apparently is intended to be used a a private task. I won't fight city hall, although I would prefer it as a separate class.

Frustratingly, the example doesn't provide the implementation of setProgressPercent. Well, drop that for a moment. We're doing the minimum here, which will in this case execute a toast in the doInBackground.

private class DownloadFilesTask extends AsyncTask {
protected void doInBackground(URL... urls) {

// some toast code
}

protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}

protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}


As the docs say,

The three types used by an asynchronous task are the following:

Params, the type of the parameters sent to the task upon execution.
Progress, the type of the progress units published during the background computation.
Result, the type of the result of the background computation.

So, what's going on with this?

'AsyncTask enables proper and easy use of the UI thread.

This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers."

Ok, no problem.

"An asynchronous task is defined by a computation that runs on a background thread and whose result is published on the UI thread. "

Ok, but how to publish the results?

"An asynchronous task is defined by 3 generic types, called Params, Progress and Result, and 4 steps, called onPreExecute, doInBackground, onProgressUpdate and onPostExecute."

Ok, so the Params, Progress and Result actually are the input params to the Async Task, and you can define what they are. The class you define is a subclass of Async Task, and it step through the 4 steps defined. I'm not quit sure how the on progress update works.

"Usage

AsyncTask must be subclassed to be used. The subclass will override at least one method (doInBackground(Params...)), and most often will override a second one (onPostExecute(Result).)"

Ok, so it definitely has to override doinBackground(Params...), and also do something once that part is completed.


"Here is an example of subclassing:

private class DownloadFilesTask extends AsyncTask {

// so the URL is input to the doInBackground; the Long is returned by that, and passed into postExecute. The Integer is passed
// into onProgressUpdate - by the publishProgress?

protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
}
return totalSize;
}

// This is called back, probably by the publishProgress...where is setProgress percent defined?
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}

// this is the post execute
protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}

"Once created, a task is executed very simply:

new DownloadFilesTask().execute(url1, url2, url3);"

Ok, that makes sense.

Try running with toast, but that kills it.

"AsyncTask's generic types

The three types used by an asynchronous task are the following:

Params, the type of the parameters sent to the task upon execution.
Progress, the type of the progress units published during the background computation.
Result, the type of the result of the background computation.

Not all types are always used by an asynchronous task. To mark a type as unused, simply use the type Void:

private class MyTask extends AsyncTask { ... }"


private class DownloadFilesTask extends AsyncTask {
protected Long doInBackground(URL... urls) {


So, when you give something like URL above, it's saying doInBackground will receive a list of Urls. The syntax "URL... urls" is probably a Java 1.5 thing and clearly specifies a List with the named list afterwards.


"The 4 steps

When an asynchronous task is executed, the task goes through 4 steps:

onPreExecute(), invoked on the UI thread immediately after the task is executed. This step is normally used to setup the task, for instance by showing a progress bar in the user interface."

Ok, I would like an example of the progress bar.

"2, doInBackground(Params...), invoked on the background thread immediately after onPreExecute() finishes executing. This step is used to perform background computation that can take a long time. The parameters of the asynchronous task are passed to this step. The result of the computation must be returned by this step and will be passed back to the last step. This step can also use publishProgress(Progress...) to publish one or more units of progress. These values are published on the UI thread, in the onProgressUpdate(Progress...) step."

Right, as I suspected, it's a call to the onProgressUpdate. Although in the example, it doesn't explain why the primitive type is converted into the the first value in an array of integers.

"onProgressUpdate(Progress...), invoked on the UI thread after a call to publishProgress(Progress...). The timing of the execution is undefined. This method is used to display any form of progress in the user interface while the background computation is still executing. For instance, it can be used to animate a progress bar or show logs in a text field."

The big thing I will need for this is to figure out how far along the file is in being downloaded; wait, I think it's loop driven by bytes. That's the ticket.

"onPostExecute(Result), invoked on the UI thread after the background computation finishes. The result of the background computation is passed to this step as a parameter."

This would be unzipping the file, and maybe showing some kind of download complete method.

Ok, I got the "calling time service message to display.

Now, I have to figure out how to incorporate the file download.

Save that for later!

Thursday, May 26, 2011

Lean startups and Ash Maurya

Yesterday, I attended a talk by Ash Maurya on lean startup techniques held at HotSpot in Cambridge, and sponsored by the VC company Matrix Partners. It went from 2-6 with a cocktail hour afterwards.

I've read here and there about lean startups, 37signals, Steve Blank's "get out of the building" advice etc. Since I wanted to get the most of of the workshop, I checked out Ash's site the night before and immediately read the first chapter of a pdf he is promoting called "Running Lean". There he presents a one-page business plan template called a "Lean Canvas, which I immediately started filling out relating to my super-duper hot Japanese Quiz android application. Also, I realized at a more fundamental level that I hadn't done things in the right order - I'd gone ahead and built the software, which has taken about 2.5 months, before going out and talking to the customer. Bzzzt!

What you need to do according to the lean methodology is go through several iterations of customer interviews before actually building the product. Because, if you do that, you will have successfully identified what resonates with them - which is unlikely to be the same as what resonates with you. And they will be your early adopters and give you traction.

The nice thing about "Running Lean" is it gives a very precise map for actually implementing the lean startup technique. I would say the two most important things about the book are the "Lean Canvas", a one-page business plan, and the carefully laid out map each step of the lean startup process, in particular when and how to involve the customer. Also, very specific examples are provided on *how* to interview our prospective customers. In summary, this book is a nice quick read which defines a map for implementing the techniques needed to be a lean startup.

These concepts were essentially what Ash presented during the afternoon, which helped me to review them, as I had sat up reading the book late that night and probably missed some key points.

Another cool thing is that Ash offers a 30-minute interview with purchase of the book, or even if you attend his workshops, apparently. I spoke briefly with him afterwards, and he suggested that I take the filled-out-in-paper Lean Plan using his online tool, and contact him, which I will certainly do. He was even patient enough to check out my application. He's clearly a pleasant, intelligent and focused guy.

Afterwards, I wandered through the cool brick and glass Hotspot headquarters and outside to a beautiful patio surrounded by greenery and flowers, which was just about the right size for the 100 or so people who attended the lecture. I spoke with several entrepreneurs or would-be entrepreneurs. One chap already has a side business making tens of thousands of dollars per year; another was a student with a very good idea. Also by pure luck I chanced into a brief conversation with Tim Barrows, who is actually the head of the Matrix Partners VC firm. A distinguished-looking gentleman with a pleasant English accent, we chatted briefly about my Japanese app, and also about a company he knows about which is doing memory-related software. Although he was soon swept away by another group, it was kind of fun to have been talking to an actual VC angel. It's a first for me.

The hors d'ouvres served throughout were first class, and the glass of red wine I had was really pretty good, although I'm not a connoisseur. Finally, as much to prevent myself from chowing down the hors d'ouvres as anything else, I headed home. It was definitely an afternoon well spent.

Quick bug fix (hopefully) on program restart

This should be a quick post. Last time I tested restarting my app from the question display - I got the "Sorry - the application has stopped unexpectedly, please try again.

Well, this proves to be repeatable. So, let's take a look at logcat. Ah-ha. The trace shows that it went south in our old friend getWordGrouping. This is an in-memory hash table of similarly grouped words which are used to provide candidate answers to quizzes. Clearly, the problem has to do with the fact that it's not getting re-initialized when the activity resumes.

This gives me a chance to straighten out an annoying lack of symmetry in the code. Now that I've restructured, I no longer have to worry about resetting the quiz sequence; it limited to the gap between start and end quiz numbers, and that shouldn't be too high. Hmmm...questions answered could go a bit off; I'll have to check that.

Hmmm - I don't get it. At first I put in the init words, but I had some inconsistent results, so I took it back out - and now I can't repeat the error.

Well, the problem seems to be, if I kill it in the middle of a sequence, the same words are done over again. That's ok if the sequence is small, but what about a large sequence?

That I don't care about. But, the real problem is, once they finish that sequence, it goes back to that sequence one more time before it moves to the next one. Why would that be? Where does the update happen, and why is it reverting to the old numbers? Am I not saving them on the update? Ah, yes that was it. Let's put the save in and try again.

Ah, ok, now it's crashing again - as it was supposed to, because I didn't fix that part. If I restart again, it works, because it goes through the start display, and the numbers were properly incremented.

Now Let's just add the code that will fix the word hash setup, and try again.

Perfect. On the third answer and kill, it came back to the same question, then wrapped and went to the next question.

Now try on the third answer, before answering the question...

Good - it wrapped again.

Now, we'll try it on the answer side of the second question...

Whoah. Crash! Somehow it got into the onclick, although that had already happened. I was on the answer side.

So, num questions asked was less than num questions in seq, and it went to increment the question - except - there was an index out of bounds exception, because nothing is in quiz state. Clearly the problem here is that it didn't enter through the onResume method - but rather through the onClick, which somehow got re-queued.

Sooo...the obvious answer is to re-initialize the state, just as in on resume. We'll extract the relevant calls to a method, and retest.

Crash again! Ah, I misspoke when I said the click got queued - it came back to the question, not the answer. I pushed the click. So, remove the extracted method from the onclick, and find out why the method is wrong, the one time it is called.

I'm starting to thing I should just call initialize, as I had originally planned.

Right now, it's doing something like this:

InitUtils.setAppState(this);


mAppState = (AppState) this.getApplication();

if (mAppState.currentQuestion == 0) {
incrementQuestion();
}

InitUtils.initializeWordGroupings(this, mAppState);

Initialize does this:

public static void initialize(Activity a) {

AppState appState = (AppState) a.getApplication();

initializeWordGroupings(a, appState);

appState.initQuizSequence(a, appState.quizStartNum, appState.quizEndNum);

Log.d(TAG, "exit initialize");

}


The other place where initialize could be called from, StartActivity, is this:

InitUtils.setAppState(this);

InitUtils.initializeWordGroupings(this, appState);


It's not initializing sequence either; so, that's just plain wrong. Well, leave it for now - change as few variables as possible. Come back to it after QA code works.


This will be initialize:

public static void initialize(Activity a) {


AppState appState = (AppState) a.getApplication();

InitUtils.setAppState(this);

initializeWordGroupings(a, appState);

appState.initQuizSequence(a, appState.quizStartNum, appState.quizEndNum);

Log.d(TAG, "exit initialize");

}

And after it's called, we'll check the counter for zero in question activity.

Whoah - it crashed again! This time,

E/AndroidRuntime( 4890): Caused by: java.lang.NullPointerException
E/AndroidRuntime( 4890): at com.jlptquiz.app.QuestionActivity.onResume(QuestionActivity.java:64)
E/AndroidRuntime( 4890): at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1150)
E/AndroidRuntime( 4890): at android.app.Activity.performResume(Activity.java:3832)
E/AndroidRuntime( 4890): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2110)
E/AndroidRuntime( 4890): ... 12 more


Naturally, on the onResume, line 64. I lost the initialization of appState when shuffling the code in and out of that method.

Ok, I went through one level, and now have stopped at the second one. Let's kill and restart.

Well, it restarted on same question ok - but then went on to ask exactly the same question. This is a bug I'm going to have to live with - because i don't want to get into saving and restoring the current sequence. It's on a restart condition, it's a mobile app, and it's not worth the complexity it would introduce.

Ok, now I'm going to kill it on the question part of the 2nd of 3 questions.

Actually, it came back to the same question. I'm wondering which is better - to skip a word by potentially selecting the same question; or come back into the middle of a sequence, and then say you've advanced, and then go back to the original sequence you were on? The second option looks like a mistake - but so doesn't going to the next question and having it be the same question. What I would prefer as a user is to err on the side of caution repeat the questions. Definitely.

Actually, instead if going to second question, where I killed it, it went back to the first, ran through the whole level again, then went to the next level; which is actually the best solution now that I think about it. But, why? Let's confirm that behavior. Kill it on the second question, no second answer.

Goes back to the second question...than asks the same question again - and increments to the next set of questions. Did I forget to relaunch it?

Let's do it again. Relaunch.

Restart on the second *answer*. Goes back to the same question. Asks the first question (only 1 in 3 chance of getting the one that wasn't asked) - says congrats - and it advances! Did I "undo" the save on the wrong part? Why can't I find it? If the num questions asked == the last quest number - the first quiz number, go to the next set. It would be on the button push. Where's the button push? Find the click.

There is is - in incrementQuestion, which comes from checkIncrementQuestion, which comes from onClick, which comes from onSetupButton.

Same result, save there, or not there. It doesn't seem to make a difference, either way. It might have been that way before, I was testing it on the question, not the answer? I don't think that would make a difference. Well, I'm a bit confused, but the more I think about it, whether I save it or not, it's going to get to the end of the current sequence of questions and jump to the next. Sooo, I might as well save it, just to be consistent.

Ok, now I'm in the second answer again. Kill and restart. And of course it advances to the same question, then goes to the next level, missing whatever the third one was. Actually - if I don't save it - you'll get two shots at the getting the question instead of one. I'm going to flop again, and comment out the save.

Kill on the second question...and it comes back to the same question, and answer it. Then it asks for what was the first queston again - now, if i didn't save it, it should ask one more after this? No, because it had asked 1 already when I killed it. So, this *is* that "second" shot, i.e. the third question. Well, it's better to have at least one shot at getting that unshown question.

Ok, I finally have the behavior defined, even if it's not perfect. Ideally I'd like to reset it to the beginning on a destroy. But what if there's a loong sequence. I just had a thought - when I kill and restart, it's coming in through start activity anyway. So, why not just reinitialize the sequence there? Actually, that's happening when it goes into question. Wait - wouldn't that mean that questions could be duplicated? No, I was wrong, it isn't coming through start every time. But how is it possible than questions. aren't getting duplicated if it's re-initiated the sequence every time? Oh, right, because it's not part of the question loop. Right.

Ok, now, the question is, why am I initializing *anything* in start activity? It's just handing off to question activity, which does the same thing. Well, not necessarily. It also hands off to settings. And what does settings do? Let's take a peek. Right, it depends on something being in settings. Hmm. I could just do a setAppstate, which sets the defaults.

I'll leave it for now. I'll put a comment for a posible refactor later.

There. That turned out to be a bit tricker than I anticipated. It's not perfect, but it's satisfactory.

Submitting an app to the iPhone App Store

Well, I had previously agreed to submit an iPhone app to the app store for a client I developed simple quiz application for. It took a while to get to this point because my client had difficulty getting a developer's certificate from Apple for his corporation. He finally submitted for it under a personal licence, so here I am.

So, let's see. SO has a page on how to do it - let's look at that.

"Start by visiting the program portal and make sure that your developer certificate is up to date. It expires every six months and, if you haven't requested that a new one be issued, you cannot submit software to App Store."

Ok, I have one and my client has one. This is under his name. Let's see how this works. Ok, it says "login to the developer's portal. click login.

Ok, it says "Sign in with your apple id".

Ok, I logged in.

"Open your Xcode project and check that you've set the active SDK to one of the device choices, like Device - 2.2. Accidentally leaving the build settings to Simulator can be a big reason for the pink rejection. And that happens more often than many developers would care to admit."

I better plug in my device and make sure it runs. Ok - it's syncing with iTunes - which I've just upgraded. How's that going to affect this? What exactly will iTunes sync up? Photos? Mp3s?

Man, it's hot here. Panera bread. It's hard to figure out which way these things are going to go. I was freezing at the Panera the next town over the other day.

I have to stay focused. Just wait this one out. There's a baby crying. I need to get some exercise. I wish I could play some tennis, but my rackets are being restrung.

Ok, the itunes icon is jumping up and down. Good, both systems are showing sync in progress. Ok, xCode needs to collect debugging information on my device. Why? I've debugged before on the device.

Hmmm. No provisioned IOS device is connected. Is it on? Dark, locked. Try again.

Same problem. Ok, so, what do I do now? I know I had it running on the device before. Oh, I see - I logged on with my customer's account. I have to log on with mine.

What is my account? Ok, got it. Now it runs. Nice and simple. Will I need to add the device to my client's profile to upload it?

Let's see. Ok, Xcode is set on device. And I put it on release instead of debug.

"Next, make sure that you've chosen a build configuration that uses your distribution (not your developer) certificate. Check this by double-clicking on your target in the Groups & Files column on the left of the project window. The Target Info window will open. Click the Build tab and review your Code Signing Identity. It should be iPhone Distribution: followed by your name or company name."

I need to log back into my client's account now. Ok. Do I need to add a device profile for my device to upload it? I don't know. I'm going to go ahead and try to upload.

"Check this by double-clicking on your target in the Groups & Files column on the left of the project window."

Ok, I've got it.


"The Target Info window will open. Click the Build tab and review your Code Signing Identity."

Ok, I'm there.

"It should be iPhone Distribution: followed by your name or company name."

It is followed by my name, not my customer's name. So, now what?

From what all I can figure out, I need to create a certificate-signing request in the customers name. This is under:

http://mobiforge.com/developing/story/deploying-iphone-apps-real-devices

"Generating a Certificate Signing Request

Before you can request a development certificate from Apple, you need to generate a Certificate Signing Request. This step must be performed once for every device you wish to test on. To generate the request, you can use the Keychain Access application located in the Applications/Utilities/ folder (see Figure 2)."

Ok, I'm there.

Next, "In the Keychain Access application, select the Keychain Access > Certificate Assistant menu and select Request a Certificate From a Certificate Authority (see Figure 3). "

Check.

In the Certificate Assistant window (see Figure 4), enter your email address,

Check (client's address)

"check the Saved to disk radio button and check the Let me specify key pair information checkbox. Click Continue.

Check and check.

"Choose a key size of 2048 bits and use the RSA algorithm (see Figure 5). Click Continue."

Got it.

"You will be asked to save the request to a file. Use the default name suggested and click Save (see Figure 6)."


Done.


"Logging in to the iPhone Developer Program Portal

Once you have generated the certificate signing request, you need to login to Apple's iPhone Dev Center (see Figure 7). Click on the iPhone Developer Program Portal link on the right of the page. Remember, you need to pay US$99 in order to access this page."


Ok, I am on his account.


"In the iPhone Developer Program Portal page, click the Launch Assistant button (see Figure 8) to walk you through the process of provisioning your iPhone and generating the development certificate."


Let's get to figure 8.


Ok, he apparently didn't give me the correct password, or somehow didn't complete the registration process. Ouch.

Here's the link I'm following:

http://mobiforge.com/developing/story/deploying-iphone-apps-real-devices

I'll pick it up tomorrow, once the signon situation is resolved. Is the certificate wrong?

Monday, May 23, 2011

Sound and frequency

One of the most pleasant aspects of the Japanese Vocabulary program I'm working on is the fact that a couple of the levels actually have audio files to go along with visuals. Fortunately, I've already implemented this in the "AnswerActivity", and just have to transfer the functionality to the Question Activity (which now includes the answer as well).

Since it's mostly static functions, or functions that ought be be static functions, and since the Question Activity class is getting kind of big, I just took all the audio play functions and stuck them into a "PlayAudioUtils" class. Then, I just move the call to play Audio from the Answer to the question activity - and done. One of the easiest changes I've ever made.

How to use frequency of use is a bit of a stickier wicket. The program randomizes the order in which the questions are asked, so what's the point of frequency? Well, the program does provide the ability to specify the range of questions in a quiz. Up till now, that meant that if you limited your quiz interval to say, 25 questions, from 1 to 25, you'd get, say, all the words beginning with A. The change we're going to implement here is to change that first set to the most frequently used 25 words, arranged at random, and the second second set the next 25 most used words, and so on. This hopefully will be a quick change.

Hmm, that's strange. It seems like it's already being read in by frequency:


public ArrayList selectByLevel(int levelIn) {


String strLevel = Integer.toString(levelIn);


Cursor cursor = this.myDataBase.query(VIEW_BY_LEVEL_FREQ, new String[] {
"level", "number", "kanji", "hiragana", "english" },
"level = ?", new String[] { strLevel }, null, null, null);

ArrayList rows = new ArrayList();

if (cursor.moveToFirst()) {
do {

int level = cursor.getInt(0);
int number = cursor.getInt(1);
String kanji = cursor.getString(2);
String hiragana = cursor.getString(3);
String english = cursor.getString(4);
if (kanji.length() == 0) {
kanji = hiragana;
}
Question row = new Question(level, number, kanji, hiragana,
english);
rows.add(row);
} while (cursor.moveToNext());
}

if (cursor != null && !cursor.isClosed()) {
cursor.close();
}

return rows;
}

This is called from initialize questions...ah, but that only stores in the word groupings now. What's driving the order of the words that go into the randomization function? What/where was that rnd function?


It's got to be something in here, in initializeQuestions:


AppState appState = (AppState) a.getApplication();

initializeQuestionsSQL(a, appState);

appState.editRangeNumbers(a);

appState.initQuizSequence(appState.quizStartNum, appState.quizEndNum);

And, here's el problemo:

public void initQuizSequence(int startNum, int endNum) {

Log.d(TAG, "initQuizSequence");

quizSequence.clear();

for (int i = startNum; i <= endNum; i++) {
quizSequence.add(new Integer(i));
}
Log.d(TAG, "exit initQuizSequence");

}


So, frequency isn't playing a role - it's base on setting up a sequence in numeric order, and using those sequential number to determine the order of the the words accessed.

With frequency, the original "id" number is no longer much good except for directly identifying and accessing a word. And since there may be gaps in the frequency number, due to being split between jlpt levels and whatnot, the order should be driven by the level freq view. So, it's going to have to have a way of getting to the nth record through the nth + m record. The best solution may be a query with ? parameters.

Let's add that to the DataBaseHelper.

No, actually, we've got to run sequentially through all the available rows until we get to the number of records requested.

Let's try something like this:

public void initQuizSequence(int startNum, int endNum) {

Log.d(TAG, "initQuizSequence");

quizSequence.clear();

DataBaseHelper dbHelper = new DataBaseHelper(this);

ArrayList rows = dbHelper.selectByLevel(jlptLevel);

int cntr = 0;

for (Question question : rows){

cntr++;

if ((cntr >= startNum) || (cntr <= endNum)) {

// question.number is like an id
quizSequence.add(new Integer(question.number));
}

if (cntr > endNum){
break; // no need to go further
}
}
Log.d(TAG, "exit initQuizSequence");

Ok, after an unnecessary amount of debugging because I didn't copy the database access from someplace else, I have now run into a crash on a null pointer on "omou"..

Plus, my carefully worked out scheme for highlighting the correct answer seem to also be randomly highlighting some other answers.

One thing at a time. I think I will turn off the shuffle for now. And then see where it crashes.

Ok, it crashed on npe just after "hodo". A dump of the database show it has a number, so the db is not the problem. That's the return rows, and it shows nothing was added to rows - the selection criteria wasn't met.

Ok, the number of rows returned is zero. Why? Ok, it's getting it from the quiz sequence index, which is from start number to end number.

Then end number - is 10. Ahh.

The problem is I can have this situation:

Start number = 25
End number = 50

So, then I have to calculate the difference between those two; and compare that with number questions asked.

If it's greater, then I show them the score page and do something from there.

Otherwise, I also have to check if the end number exceeds the nag number; and if so, display a nag message or something.


Ok, here's the logic now:

if ((appState.quizEndNum - appState.quizStartNum) <= appState.numQuestionsAsked) {

Context context = getApplicationContext();
int duration = Toast.LENGTH_LONG;
CharSequence text = "Quiz Complete; restarting";
appState.quizSequenceIndex = appState.quizStartNum;

Toast toast = Toast.makeText(context, text, duration);
toast.show();

InitUtils.clearQuiz(this);
finish();

} else {

incrementQuestion();

}

It's till crashing; I think it must have something to do with clearing the quiz, i.e. the quiz sequence, and not re-initializing it. I tried tossing in a finish, to take me back to the initial activity. I think it makes sense, but I'm getting some message like finish already executed.

Well, I don't need to re-initialize the word array, but I do need to reinitialize the sequence. Let's see if that's called from anywhere it needs to to be called from - like the start quiz button.

Ok, the first thing I'm going to try is moving the initialize activity to the start quiz button. This will set me up for returning to this display when the quiz is complete and having everything handled from there.

That seems to work ok. Now, let's figure out why it's not getting back there.

Ok, I deleted the clear quiz logic; that should now be handled by the start quiz button. I still don't get why I'm not seeing the toast message; maybe it crashes too soon. Let's try it again.

Yes, that did it. It's now displaying the toast message, "quiz complete", and returning to the start activity. There the customer can change the settings or go right back to the same quiz. Awesome. That's a wrap.

Sunday, May 22, 2011

Formatting - some tricky stuff

Today, I'm going to tackle an issue I've long been putting off. I need to get some aesthetically pleasing lines around the potential choices in my possible answers. I had taken out the separator lines based on a suggestion, but It's too confusing. Or, I could re-implement the radion button - but you can press on the whole bar.

http://stackoverflow.com/questions/2372415/how-to-change-color-of-android-listview-seperator-line.

For now, I'll change it to his:

android:cacheColorHint="#FF000000" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:textColor="@color/text_color"
android:divider="#FF000000" android:dividerHeight="4sp"
android:focusable="false">

Ok, I got the lines, but they're black. Worse, the top and bottom aren't covered.

I'm having some trouble figuring how to set the border, as opposed to divider line. A border will get the top and bottom of the list. Is it the view, or the ListView? Let's look at some apis from Eclipse...

Well, the view doesn't seem to offer much hope. How about ListView?

As usual SO to the rescue:

http://stackoverflow.com/questions/2658772/android-vertical-line-xml

Something like:

android:background="#333000" />

Ok, next up, is how to get a scroll vie around the text display; sometimes the text is too long and pushes the button off the bottom of the display.

We'll try the selected answer from http://stackoverflow.com/questions/1748977/making-textview-scrollable-in-android:

"You don't need to use a ScrollView actually. Just set the android:maxLines and android:scrollbars = "vertical" properties of textview in layout xml file. Then use the TexView.setMovementMethod(new ScrollingMovementMethod()) in the Code. Bingo!!!! It scrolls automatically w/o any issues.

But surely maxLines requires you to enter an arbitrary number; this isn't something that will work for every screen size and font size? I find it simpler to just wrap it with a ScrollView, meaning I don't have to add any further XML attributes or code (like setting the movement method). – Christopher Dec 19 '10 at 22:57"

Let's see what happens if we just use scroll view.



android:layout_width="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="0dp" android:layout_gravity="center_vertical|center_horizontal"
android:textSize="30sp" android:textColor="@color/text_color" />


No, I think I need to set maxlines.

Ok, I ended up with this:

android:background="#333000" />
android:layout_height="100sp" android:maxLines="3"
android:scrollbars="vertical">
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:layout_gravity="center_vertical|center_horizontal"
android:textSize="30sp" android:textColor="@color/text_color" />

I'm going to tackle the space race in this post. First, I've been getting warning messages about lack of space. So, how can I look at this on my Nexus one?

These are the questions I need answered:

How much space is being used on storage on the phone's regular, internal memory?
How much space is available?
Where are the .apk files stored?
Are they unzipped? If so, where is that data stored?
How much memory is available on the SD card in total?
How much information is being stored on the SD card?
How much ram/memory do I have?

Let's look at settings.

SD Card: Total space 14.81 GB
Available space: 13.81 GB.

Thats good.

Internal storage: Available space: 5.91 MB
So, how much total space? I'll google it.
ok, on the nexus one, there's 512 MB, 180 or 220 available at the start depending on which post you read.

So with the 5.91, I've used up most of it.

Then there is this comment:

I've installed APPS2SD, and it calculates my total storage as roughly 220MB?

Here's one other thing to tackle - this message when I install my app:

INSTALL_FAILED_INSUFFICIENT_STORAGE

This post addresses it:

http://stackoverflow.com/questions/4709137/solution-android-install-failed-insufficient-storage-error

android:installLocation="preferExternal"

e.g.

package="com.andrewsmith.android.darkness"
android:installLocation="preferExternal"

Let's delete the app and rerun.

Hmm...my app is taking up 12.29 MB? Ouch. Is it the sound files, or the database file, which is copied to SQLlite, or all 3? Or maybe it doesn't even include the sound files, which I installed manually. And are the sound files on the SD card?

The application is 4.61 MB; the data is 7.68 MB.

Ok, I just noticed that on the bottom of the manage apps display, it shows the internal storage on the bottom. Right now, it's showing 178 MB used, 18 free. So, it looks like I have a total available for *me* to use of 196 out of the 512, which is consistent with info at the start of this blog entry.

There's also a tab for SD card, which shows the apps on there

But, the "preferExternal" doesn't seem to work. I'm still getting the insufficient storage error. Well, let's clean up some more apps.

Ok, there we go, it's running. But, the app didn't install on the SD card.

It seems it may not be possible, see:

http://stackoverflow.com/questions/3584297/installing-application-on-sd-card-in-android-sdk-2-2

Friday, May 20, 2011

Dumping the Answer display


One of the main structural issues with my quiz app is that answers are shown on a separate display than the questions. This has the effect of swinging the customer back and forth between the two displays. Since I first coded the app, I've realized that it would be a more pleasant experience to just keep the customer on the same display, and simple highlight the good answer in green, and the wrong answer in red, if they chose incorrectly.

The thing about Japanese is that you have the kanji, which is a symbol, then you have the phonetic spelling, which is best displayed in the Japanese alphabet. But then there is also the English translation, which is a little curveball on a standard quiz. Since the phonetic spelling is already displayed, on a standard quiz it is simply a question of highlighting the correct answer. But also displaying the english translation on the same display is more problematic. Plus, I'll need to add the "next question" button.

It seems like it might get crowded. And if I put ads in, it will be that much more crowded. But, it might be worth it, simply to keep from bouncing the customer from back and forth from the question to the answer display and back all the time.

One thing I can certainly do is make the fonts smaller. I went a little big on them.

Ok, I've made some progress. I now have a button that keeps me on the same page, it just gets the next question and possible quiz answers and displays them. And when you select an answer, it will display the english.

Now, I need to highlight the correct answer. I've already done some highlighting on the selection. How did that work, before?

Ah, here we go - it's in "custom_list.xml".


xmlns:android="http://schemas.android.com/apk/res/android">



android:startColor="@color/state_pressed_end"
android:endColor="@color/state_pressed_start"
android:angle="90" />
android:width="3dp"
android:color="@color/grey05" />
android:radius="3dp" />
android:left="10dp"
android:top="10dp"
android:right="10dp"
android:bottom="10dp" />





android:endColor="@color/state_focused_end"
android:startColor="@color/state_focused_start"
android:angle="90" />
android:width="3dp"
android:color="@color/grey05" />
android:radius="3dp" />
android:left="10dp"
android:top="10dp"
android:right="10dp"
android:bottom="10dp" />






That's all well and good. But those highlights are encoded in XML, and triggered based on touch events. I need to programmatically set a color; one for the correct answer, and another for the selected, incorrect answer if they're different.

This might be a good place to start:

http://tinyurl.com/3e5ya32

rowView.setBackgroundColor(R.drawable.view_odd_row_bg);

But where did row view come from? Some additional hunting around reveals this url:

http://ykyuen.wordpress.com/2010/03/15/android-%E2%80%93-applying-alternate-row-color-in-listview-with-simpleadapter/

Which has this code:

public void onItemClick(AdapterView parent, View view, int position,

long id) {

// TODO Auto-generated method stub

Log.d("debug", "postion: " + position);

Log.d("debug", "id: " + id);

}

};

It's not clear if view is the whole list view, or that one row.

List's try this:

view.setBackgroundColor(Color.BLUE);

Good. That worked.

But, that's not enough. I need to also highlight the *correct* answer. Obviously, position will play a role here. All I need to do is apply it to something.

There is a parent, the ListView, with a getChild method. So, I need to get the view that's at the correct position.

Let's try this:

View correctRow = l.getChildAt(1);

correctRow.setBackgroundColor(Color.GREEN);

Oddly, it always seem to choose row 3 for the highlighted color. The blue highlight is unpredictable.

Let's change the number:

View correctRow = l.getChildAt(0);

Zero highlights row 4.

I'd best check the API. It just says, it returns the view at the specified postion in the group.

What if I put a 2 in? Well, that one highlights the second row.

Let's try a 3. Yup, it highlights the first row.

So it like a reverse relationship between what you'd expect and child index.

expected actual
0 4
1 3
2 2
3 1


Maybe it's a stack kind of a thing?

Let's see what postion it is...

Toast.makeText(getApplicationContext(), "postion is: " + position,
Toast.LENGTH_SHORT).show();

That's exactly as expected. It's probably the fact that the view is a node, and the node is retrieved list-in, first out. So I'll just twiddle the code to handle it.

Ultimately end up with code like this:

@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);

int childCount = l.getChildCount();
int lastViewPos = childCount - 1;
View selView = l.getChildAt(lastViewPos - position);

// http://www.ceveni.com/2009/08/set-rgb-color-codes-for-android-and-rgb.html
// Light Coral 240-128-128 f08080

selView.setBackgroundColor(Color.rgb(240, 128, 128)); // light red

int i = findCorrectViewPosition(l, childCount);

View correctRow = l.getChildAt(lastViewPos - i);

// Pale Green 152-251-152 98fb98
correctRow.setBackgroundColor(Color.rgb(152, 251, 152));
}

private int findCorrectViewPosition(ListView l, int childCount) {
// find correct answer
int i = 0;
for (i = 0; i < childCount; i++) {
View view = l.getChildAt(i);
String str = (String) ((TextView) view).getText();
if (str.equals(mQuestion.hiragana)) {
break;
}
}
return i;
}


And no more answer display! Of course, there is a lot of cleanup work to do. I'll get to that later.

Thursday, May 19, 2011

The nag

Instead of blogging as I post, this time I'm doing as an after the fact, just for a change of pace.

In this case, I wanted to institute a nag message in the settings validation, in case the potential customer tries to enter a number which is greater than the actual range of words available in the free edition.

A way to determine if it's paid or not is to simply check the total number of words against a "nag" number of words available in resources. The number of words will always be greater then the nag number in the free version. If the customer tries to enter a greater number, he will not only get the error message stating the number is too high, he will also get another message inviting him (or her) to purchase the paid app.

The problem turned out to be that I was unable to cover both conditions in a single test run. I did try to set an attribute on the activity, retrieved from robotium's "getCurrentActivity" message, but that invariably causes the test to fail regardless.

However, the good news is I was able to obtain the row count (number of words) and the nag number using standard api's from robotium, like so:


private int getRowNumberInfo() {
Activity a = solo.getCurrentActivity();

DataBaseHelper dbHelper = DataBaseHelper.createDB(a);

int lastNum = dbHelper.getLevelCount(5);

dbHelper.close();

int compNum = lastNum + 1;

/* get max Number */
int nagNumber = InitUtils.getNagQuestionNumber(a);

if (nagNumber < compNum) {
compNum = nagNumber;
}
return compNum;
}



Here's the production code:

if (quizEndNum > compNum ){

valid = false;

String message1 = "Quiz end number must be less than or equal to " + compNum;
String message2 = "Please check out our paid version on the Android Market place for the all the Vocabulary";

Toast.makeText(getBaseContext(),
message1,
Toast.LENGTH_LONG).show();

if (!paid) {

Toast.makeText(getBaseContext(),
message2,
Toast.LENGTH_LONG).show();

}

}

And finally, the test:

public void testEndQuizNumTooHigh() throws Exception {

int compNum = getRowNumberInfo();
SettingsActivity a = (SettingsActivity) solo.getCurrentActivity();
int nagNumber = a.getmNagNumber();

boolean paid = true;
if (nagNumber < compNum) {
paid = false;
}

solo.clearEditText(0);
solo.enterText(0, "5");

solo.clearEditText(1);
solo.enterText(1, "1");

solo.clearEditText(2);
solo.enterText(2, "1000");
solo.clickOnButton(0);
assertTrue(this.solo
.waitForText("Quiz end number must be less than or equal to "
+ compNum));
assertTrue(this.solo
.searchText("Quiz end number must be less than or equal to "
+ compNum));


if (!paid) {
assertTrue(this.solo
.waitForText("Please check out our paid version on the Android Market place for the all the Vocabulary"));
assertTrue(this.solo
.searchText("Please check out our paid version on the Android Market place for the all the Vocabulary"));
}


}

One final note - I changed the starting activity from StartActivity to SettingsActivity, so getCurrentActivity would return the current activity.

Completing the validation logic? Really?

After fussing with the database logic, all to get rid of those peskery (is that a word) database not closed messages, I'm planning to go back and code them properly. But, I'll do it as I run across them, because it's working now and I'm keen to complete the validation part.

One great benefits of the validation routine is I can ensure I have the correct level before continuing. Also, the SQL database now means that I don't have to re-initialize on a change of level. Well, I guess I could've always loaded all the words into the hash, regardless of level and had the same effect.

We can easily add a check for quiz end number:


public void testEndQuizNumTooHigh() throws Exception {

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "5");
solo.clearEditText(2);
solo.enterText(2, "1000");
solo.clickOnButton(0);
assertTrue(this.solo.waitForText("Quiz end number must be less than or equal to 669"));
assertTrue(this.solo.searchText("Quiz end number must be less than or equal to 669"));

}

For some reason, this doesn't pass. Ok, the problem was had munged the text in the code by doing the above method like this:

Quiz end number must be less than or equal to 669" + lastNum

Ok, all clear. Now, for the less than test.

if (quizEndNum < quizStartNum ){

valid = false;

Toast.makeText(getBaseContext(),
"Quiz start number must be less than Quiz end number",
Toast.LENGTH_LONG).show();

}


And the test:


public void testStartQuizNumGEEndQuizNum() throws Exception {

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "5");
solo.clearEditText(1);
solo.enterText(1, "100");
solo.clearEditText(2);
solo.enterText(2, "50");
solo.clickOnButton(0);
assertTrue(this.solo.waitForText("Quiz start number must be less than Quiz end number"));
assertTrue(this.solo.searchText("Quiz start number must be less Quiz end number"));

}

Odd. It's failing for no apparent reason. When I try to log the validation logic, I don't get anything on logcat.

Let's sprinkle around a few debug statements. And comment out all the test except the problem one.

Ok, there's the problem. "Quiz start number must be less than Quiz end number" <> "Quiz start number must be less than Quiz end number"


There were a couple of issue that snuck into these tests. They all had to do with being careful in coding. In the last one, I had an extra clickOnButton where it shouldn't be. In the source, I had given the wrong parameter name (lastNum instead of end quiz num), and actually reversed the "<" sign. So, it found a couple of errors in source, but here was extra debugging in test as well.

Ok, that more or less wraps up the validation logic. Now, I'll clean up some leftover code that's not long required. First, I'll run the Android junit tests, and commit.

Ok, let's refactor a little bit. I got rid of no longer needed flag and cleaned up related logic.

I just realized I should add 1 to the display error number to account for questions starting at 1. And sure enough it's flunking the test again. Hmmph. More debugging pure test code. I can tell by the wait time that it's not seeing it.

Ok, the problem was that I had two tests to change, not just one.

Ok, let's commit this and tackle some other changes.

Robotium - finish up the validation

I just realized - I'm converted to test-driven-development at this point. I like the fact that you bring scripted automation of tests into your work flow. I like the quality control that scripts provide. I the development technique of coding the test first and making it fail, and working from there. The simple-as-possible-test rule keeps you from getting too adventurous.

I just wish I spent less time debugging the tests, and more time working on the program itself. I'd say about 75 to 80 percent of my time is spent creating and especially debugging tests - mostly when the production code works fine. I'm hoping to bring this ratio down.

Anyway, back to the Robotium. The results yesterday were promising. Today, we're going to continue and hopefully complete validation of the Settings page. One validation I didn't do yesterday was ensuring that the starting quiz number is less than the ending quiz number. Let's get started.

Ok, first commit from last night.

Actually, I didn't test for the quiz num greater than words available, or even finish coding it, due to the db issues. Let's do that now:

dbHelper.openDataBase();
int lastNum = dbHelper.getLevelCount(inputLevel);
dbHelper.close();

if (quizStartNum > lastNum ){

valid = false;

Toast.makeText(getBaseContext(),
"Quiz start number must be less than or equal to " + lastNum,
Toast.LENGTH_LONG).show();

}



And the test - this should pass.

public void testStartQuizNumTooHigh() throws Exception {

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "5");
solo.clearEditText(1);
solo.enterText(1, "1000");
solo.clickOnButton(0);
assertTrue(this.solo.waitForText("Quiz start number must be less than or equal to 669"));
assertTrue(this.solo.searchText("Quiz start number must be less than or equal to 669"));

}

Good. It passed. But, the log shows that ever-present database not closed exception. Sigh. It actually died on the open database. Did I forget to code the create database? No, it's there. What line does the trace show it called it from? Wait. Did I have the close in an if statement? Ok, right - the first test has an invalid level, so the close doesn't happen.

Does that mean all my open database are unnecessary? Let's check out the code.

public static DataBaseHelper createDB(Activity a) {

DataBaseHelper myDbHelper = new DataBaseHelper(a);

try {

myDbHelper.createDataBase();

} catch (IOException ioe) {

throw new Error("Unable to create database");

}

try {

myDbHelper.openDataBase();

} catch (SQLException sqle) {

throw sqle;

}

return myDbHelper;
}


Read it and weep. This means I can get rid of all my database opens - they are redundant. I wan't sure, so I had left them in there. But it's obvious in retrospect. Where is that check for existing database I had put in there?

That was in the open database:

public void openDataBase() throws SQLException {

// Open the database
String myPath = DB_PATH + DB_NAME;

if (null == myDataBase) {

myDataBase = SQLiteDatabase.openDatabase(myPath, null,
SQLiteDatabase.OPEN_READONLY);
}

}

So, again, the open that's being called from the createDatbase.

Ok, well, at some point today, I'm going to go ahead and get rid of all the unnecessary open calls. But first, let's resolve this problem, which is branch logic which doesn't close the db the level is incorrect. Probably the simplest thing to do would be to call the createDB as if it were the open, just before it's read.


Like so:

dbHelper = DataBaseHelper.createDB(SettingsActivity.this);

int lastNum = dbHelper.getLevelCount(inputLevel);

dbHelper.close();

The dbHelper create creates a new instance of dbhelper when you call it. It check for the existence of the physical database, and copies it from the file if not there. then, opens the database and returns the new instance.


Let run the robotium tests. Well, they all failed on no button with 0 index. This happened yesterday, and was solved by a rerun. Let's try it.

Not this time. Why? I could clean the project, or try to get the button some other way. Let's clean, and also set the phone to it's normal starting state.

Ah. Great. this time it worked, We're back in business.

Wednesday, May 18, 2011

The joys of Robotium

In this post, we'll start adding tests for the Settings activity, which is the only one which involves user input aside from selecting an answer or pushing buttons. This is ideal for using Robotium, which as we saw in the last post makes it amazingly simple to test the entire application from a functional standpoint.

In the last test, we used Robotium to uncover a bug ironically introduced for the purposes of automated testing. This time, we'll see if we can introduce some kind of edit function to keep an invalid value from being entered in the first place.

Here's a nice, simple example of a validation function from http://piyushnp.blogspot.com/2010/03/android-edittext-validation-example.html:

String invalidStr="AB10",validStr="0998257476";

Pattern pattern=Pattern.compile("[0-9]*");

//argument to matcher is string to validate
Matcher matcher=pattern.matcher(invalidStr);

if(!matcher.matches()) // on Success
{
//invalid
//pop up ERROR message
Toast.makeText(context," Invalid Input ",Toast.LENGTH_LONG).show();

}else
{
//validation successfull
//Go Ahead !!
}


Let's change it, Actually, it's pretty simple; the level must be 1 - 5, the start number needs to be greater than zero and less than the end number, and the end number needs to be less than the amount of words available for that level.

Let's code the first test.


if(!matcher.matches()) // on Success
{
//invalid
//pop up ERROR message
Toast.makeText(context," Invalid Input ",Toast.LENGTH_LONG).show();

}else
{
//validation successful
//Go Ahead !!
}

The question is, how does the program stop the user from pressing save - disable the save button?

Also, how does the Robotium check the toast message?

Robotium looks like it's well supported, too. I immediately found several hits which answered the above question, on the Robotium google groups blog.

First, let's code it to fail:

assertTrue(this.solo.waitForText("Level must be between 1 and x"));
assertTrue(this.solo.searchText("Level must be between 1 and x"));


That died on an illegal state, possibly because this message isn't triggered until the activity is exited.

Let try a pass, just in case:

assertTrue(this.solo.waitForText("Level must be between 1 and 5"));
assertTrue(this.solo.searchText("Level must be between 1 and 5"));

Not surprisingly, it's the same error. Let's comment out the finish and see if that still happens. Yep. What does logcat have to say.

Huh - database not open? Why didn't I get that before this test? Let's comment out the test and see if that happened. Yes. Hmm, maybe I just missed it before. Let's rerun the invalid level test, this time with a valid level.

Yes, it passed. In fact, I haven't accounted for the invalid level yet. What I need to do, is to have the validation not allow the database to be accessed unless a valid value is entered.

Actually, the if (valid) else (process) structure works perfectly for keeping it on the same page on an invalid entry; the "process" part will do the finish. But, for some reason it didn't work on the first test run; I still had the same exception. Then I ran it manually, and got the error message - it worked. Then, I ran the test a second time and it passed. It seem like sometimes you have to run it a couple of times.

Let's make sure it fails.

assertTrue(this.solo.waitForText("Level must be between 1 and x"));
assertTrue(this.solo.searchText("Level must be between 1 and x"))

Great - it worked; I got an assertion failed. It takes a while because the toast takes a while to run, however. Let's see if it succeeds now.

assertTrue(this.solo.waitForText("Level must be between 1 and 5"));
assertTrue(this.solo.searchText("Level must be between 1 and 5"))l

Awesome. It passes. It seems like it's much faster on a pass than a fail.

Ok, let's put in the validation for the start and end numbers. Let's test for a negative quiz start number. First the fail: "Quiz start number must be greater than x". Fail!

Then the pass: Quiz start number must be greater than zero". No. Hmmm...run it again? No, I forgot to clear the field:

solo.clearEditText(1);

Try it again. Pass :)

Next, let's check for the end quiz number for a value that's too high. For this, we need to retrive the number of records in the database for the level. We have that:

int count = dbHelper.getLevelCount(inputLevel);

Ok, first I'll make it fail by giving a valid value. Here's the whole test:

public void testStartQuizNumTooHigh() throws Exception {

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "5");
solo.clearEditText(1);
solo.enterText(1, "100");
solo.clickOnButton(0);
assertTrue(this.solo.waitForText("Quiz start number must be less than or equal to 669"));
assertTrue(this.solo.searchText("Quiz start number must be less than or equal to 669"));

}

No good - It's crashing on database not open. Hmm. Let's back out the code changes and the test to see if it was accessing the db before. Hmm...it's still crashing. Why wasn't it doing this before?

There are a couple of accesses to the database showing in the log. It looks like it's happening on the second one. Which happens to be calling the same code that the initial crash was on. Let's comment that out, too. And it's still crashing, on the one remaining database call. Maybe this is a limitation of the test framework? Let me test it manually.

It doesn't crash manually. It won't be good if Robotium s incompatible with sqlite. Still, it's going down on the same routine; and I think there successful call before that.

Well, I commented out a couple of unnecessary database opens and closes, and it actually made things worse. Now the second test is giving me an invalid button index, while the third is still failing on an invalid state.

The good news is there seems to be a successful db call before the exception. The bad news is I unit tested all the database calls yesterday, and they all worked fine.

Ok, a rerun at least gets the second test working again.

I'll try moving the open and close back to the calling routine, although I don't see why this would make a difference. Hmmm. the call that does work is in a static method. Perhaps that has something to do with it.

Ah - an assert - fail. Right, that's what I wanted - it means it didn't crash on the db access. Somehow, pulling the open and close of the db out of the db helper and into the calling routine solves the problem. Ok. Well, those unit test I did yesterday weren't much help - they were supposed to catch something like that. Although, those results did agree with the production. So maybe the fault lays with robotium. Anyway, all it really means is moving the open and close back from the method to the calling method.

I've done it for one. Now the next 2.

Ok, I've done it for all of them. Let's run the robotium tests.

They are ok. Now, lets clear the log and run the Android unit tests. Great - everything is working.

I got jammed up for a bit on what I think was robotium being a bit picky about the state of SQLite, but now that's cleared up.

Cool, With Robotium's help, I've just implemented a great validation routine. Thanks Robotium - you guys rock. I can't be too harsh on Google - Robotium wraps their code.

Robotium rocks!

Today, we're going to add some functionality to test the entry of invalid values. Let's get busy.

First, I'll make a copy of an existing test using ActivityInstrumentationTestCase2. I know it's kind of slow, but I'm going to be accessing resources and I think I need it for that. Actually, I'm not sure. Let's try accessing a resource from a test that uses ActivityUnitTestCase. I'm having trouble with deciding. I think I just need to play it safe and follow along with the examples from Google. What were those? Oh, yeah, it was Spinner. Let's take a look at some of its code:


@Override
protected void setUp() throws Exception {
super.setUp();

setActivityInitialTouchMode(false);

mActivity = getActivity();

mSpinner = (Spinner) mActivity
.findViewById(com.android.example.spinner.R.id.Spinner01);

mPlanetData = mSpinner.getAdapter();

} // end of setUp() method definition



In the setup, it's grabbing the control it's intending to test, plus it's adapter.

// this is run only once at the start of the app,
// to make sure the project is initialized correctly

public void testPreConditions() {
assertTrue(mSpinner.getOnItemSelectedListener() != null);
assertTrue(mPlanetData != null);
assertEquals(mPlanetData.getCount(), ADAPTER_COUNT);
} // end of testPreConditions() method definition

Here it's checking to make sure everything's been initialized.


public void testSpinnerUI() {

mActivity.runOnUiThread(
new Runnable() {
public void run() {
mSpinner.requestFocus();
mSpinner.setSelection(INITIAL_POSITION);
} // end of run() method definition
} // end of anonymous Runnable object instantiation
); // end of invocation of runOnUiThread

This looks like it creates a new thread. It sets the focus on the control and sets the selection on it. I remember reading something about how you need to test UI on the UT thread - but it was done as an annotation.

Here's the rest of the method:

this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
for (int i = 1; i <= TEST_POSITION; i++) {
this.sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
} // end of for loop

this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);

mPos = mSpinner.getSelectedItemPosition();
mSelection = (String)mSpinner.getItemAtPosition(mPos);
TextView resultView =
(TextView) mActivity.findViewById(
com.android.example.spinner.R.id.SpinnerResult
);

String resultText = (String) resultView.getText();

assertEquals(resultText,mSelection);

} // end of testSpinnerUI() method definition



Ok, let's model test activity based on spinner code. I also found another link,

http://mobile.tutsplus.com/tutorials/android/android-sdk-junit-testing/

which looks like a nice simple example.

We'll try something like this:

public void testSendKeys() {

mActivity.runOnUiThread(
new Runnable() {
public void run() {
mLevel.requestFocus();
//mLevel.setSelection(INITIAL_POSITION);
} // end of run() method definition
} // end of anonymous Runnable object instantiation
); // end of invocation of runOnUiThread

/// now on level
sendKeys(NUMBER_5);

// now on start quiz number
sendKeys(NUMBER_1);

// now on end quiz number
sendKeys(NUMBER_1000);

// save
sendKeys("ENTER");

Assert.assertEquals(true, true);
}

Ok, it crashed on an index out of bounds. I noticed it looked like it wasn't clearing the fields before it populated them. It was too fast. Let's unplug an run it on the emulator, which is much slower. Woah - way too slow. Back to the device.

Ok, after a little experimentation, this sequence gives me a passing test:

// / now on level
sendKeys(KeyEvent.KEYCODE_BACK);
sendKeys(KeyEvent.KEYCODE_DEL);

sendKeys(NUMBER_5);

// now on start quiz number
sendKeys(KeyEvent.KEYCODE_BACK);
sendKeys(KeyEvent.KEYCODE_DEL);
sendKeys(NUMBER_1);
// now on end quiz numbe
for (int i = 0; i < 5; i++) {
sendKeys(KeyEvent.KEYCODE_BACK);
}
for (int i = 0; i < 5; i++) {
sendKeys(KeyEvent.KEYCODE_DEL);
}

sendKeys(NUMBER_1000);

// save
sendKeys("ENTER");

Assert.assertEquals(true, true);


Now, let's see what happens when we put a crazy value in there somewhere.

sendKeys(NUMBER_9999);

9999 is a larger number then the available vocabulary limit.


I'm just having a lot of difficulty figuring out what's going on when the test runs; on the device, it goes by too fast, and sometimes the screen size changes. Also, it's not at clear it's entering the digits I want. Plus, I know it should be crashing, and it isn't. The emulator is incredibly slow.

So, I'm going to go to plan B and try Robotium. This is a frustrating thing. A morning's worth of work, pretty much trashed. This is the downside of TDD on Android. The tools are hard to work with.

Let's see, is there a tutorial or something?

Yeah. Ok, a youtube video. Ok, download the sample project. Ok, it needs notepad list. Ok, got that. Run the test. Ok, seems to work.

Let's make a copy of that project. Ok, don't forget to change the target package name in the manifest; and the package name. Add the target package to the projects tab in settings.

ok, let's try something like this:

solo.clickOnButton(0);

Wow, very cool. Not only did it press the correct button, it went to the next activity screen, without my having to specifically load it. Multi activities, something I hadn't even gotten close to with the Android testing framework.

I like the Robotium api - all the events are easy to pick out. It supports things like clear text.

Lets see if we can enter some text...

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "17");

Ok, how to I save it?

solo.clickOnButton(0);
solo.clearEditText(0);
solo.enterText(0, "17");
solo.clickOnButton(0);

Index out of bounds - perfect! Amazing - this is so much easier than Android testing framework.

Ok, let's fix up the code that causes this problem. Suddenly, I'm back into production code. Actually - this should work for the lifecycle checks I had to comment out, which I went to such pains to create.

Let's fix index out of bounds.

Ouch. Again, adding test code to return a value is just a bad idea. Especially a hardcoded one. It was ok when I was building the function, but anything hardcoded has to be cleaned out pretty quickly. Let's comment out that code.

Pass.

The problem is, I now have tests to run from two separate projects. I wonder how many of the android junit test I could bring into this one. Either that, or just test everything functionally. I think there's some I couldn't really do that for, that really do test actual methods.

Well, that's good for now. Let's pick up some more of the great Robotium in the next post.

Tuesday, May 17, 2011

Completing the initialize

In this post, we'll (hopefully) complete the conversion of our initialize to read from the database instead of the XML files. The last remaining piece to be converted is this:


/* store question in the AppState question hash */
appState.questionHash.put(new Integer(question.number),
question);

This will be straightforward to replace - just comment it out in the existing one and place it in the new one. I'm going to hold off on a unit test for now because it's going to go away.

Ok, got no failure anywhere. Now, let's drop all calls to the original method.

And all test out to green, except one - the test of words, which has some kind of problem with coordination off the mock application object with the actual one and returns a null on the word map. Since I've got coverage on that one through a couple of other methods, as was revealed in the last post, I'll drop it with a TODO to check on why and forge on.

I'm hoping I actually no longer need the code inserted above. I'm going to check any references, and hopefully just delete the question hash from the program.

Well, first let's commit our changes.

Ok. It turns out that there are about 5 or 6 references. Most of them have to do with checking the size of the current level.

It should be easy and fast to code up a test that validates a method that pulls the number of rows in the current level from the database. I'm going to create a TestDataBaseHelper class. First I'll see if I can replicate an existing test in it, which I originally put in another test class. If that works, I'll move the rest over, then code the new test.

Yes, green. We can put all the current test database methods where they belong, in the new TestDataBaseHelper class.

I'm using the ActivityUnitTestCase because the database needs an activity, and I think it might be faster than ActivityInstrumentationTestCase2.

I'll move the rest of the tests over to the new class. Yes, it's significantly faster. I think because it relies on mocks instead of the real android api, it so doesn't need to go through the Dalvik jvm.

Ok. Now, let's code this method test first. The counts are 669 and 634 for levels 5 and 4, respectively. In fact, I can even use an existing method! Heres the test:

public void testRowCountLevel5() {

ArrayList rows = null;

try {

myDbHelper.openDataBase();
rows = myDbHelper.selectByLevel(5);
myDbHelper.close();


} catch (SQLException sqle) {

throw sqle;

}


Assert.assertEquals(rows.size(), 669);

}


Yes, it passes. Now, since I just need the count, I should add a method, probably to DataBaseHelp, return the count for a given level.

public int getLevelCount(int level) {

ArrayList rows = null;

try {

openDataBase();
rows = selectByLevel(level);
close();


} catch (SQLException sqle) {

throw sqle;

}
return rows.size();
}

And it passes. Let's check the log to see if any of those irksome "can't close database" message snuck in there. It seems ok.




It seems that we could confine the open/exception logic to within DatabaseHelper class, like so:

public int getLevelCount(int level) {

ArrayList rows = null;

openDataBase();
rows = selectByLevel(level);
close();

return rows.size();
}


And heres the shortened test.

public void testRowCountLevel5() {

int count = myDbHelper.getLevelCount(5);

Assert.assertEquals(count, 669);

}

And it passes. None of those nasty exceptions are showing up in the log, either. Let's convert the rest of the data methods and calls to do the open/close.

Good - still green. How about even bringing the create database into the the DataBaseHelper? The client of the DataBaseHelper still needs an instance, so it has to be returned, but I moved it from Utils to DataBaseHelper.

The tests do make you give you a greater level of confidence that your refactoring changes are working. However, that should be dependent on the level of coverage you have in your tests.

But, the ones I have are working, and they impact the area I'm modifiying.

Ok, all set with the database classes and tests.

Now, for my next trick - let's replace the references to the question hash with database calls.

Actually, selectByLevelAndNumber should return a single question, not an array with a single member. Let's fix that. It will involve changing a test and maybe one call.

Good. All green!

Keep on truckin', get rid of the rest of the references.

Well, I'm getting to some cases that aren't covered here. Specifically in settings, for example when the customer select a quiz range that's out of the normal range. Let's cover that in the next post.

Initialize - setting up the word table

In our last post, we set up a full selection of the words at a the level desired and tested to make sure it was retrieved. The next step is to incorporate the following two parts:


/* TODO figure out what to do about non-kanji */
if (question.kanji! = question.hiragana) {
Utils.storeWordBySuffix(appState, question);
} else {
Utils.storeKana(appState, question);

}


/* store question in the AppState question hash */
appState.questionHash.put(new Integer(question.number),
question);


This poses a couple of interesting questions. The first part is needed to group potential answers by word pattern. The second loads into internal memory the questions. We've already replaced the logic to access the the questionHash in QuestionActivity. But, should we go back to the internal memory for speed purposes? Also, are there other places where the question hash is accessed?

Also, what about the test? I guess if I just put the first part in, I can figure out how to access where it's stored, and make sure the correct question was put in. I could comment out the storage of word in the XML initialize routine we're replacing. I'm just curious how many places the initialize routine is called from.

Yeah, it's called from two places, which I'm kind of unhappy about. The second one was a kind of restore for AnswerActivity which I created to satisfy the kill/restart test, but why did I have to do it that way? Actually, that's a very good reason to go with the database on the question access - I won't have to re-initialize the question hash when the app is restarted. Although I will still have to reinitialize the word table.

So, for now, I'm going to just duplicate the call to initialize in both places. like so:

initializeQuestions(a, appState);
initializeQuestionsSQL(a, appState);

First, let's change the input parameter to Activity from a specific activity and check that that works.

Ok, here's where my use of a member in a specific activity to pass a test comes back to bite me. A generic activity doesn't have that member. Ok, let's put that return paramater into AppState

Good. It passes that test.

Now, let's find out where the words are stored...

if (question.kanji != question.hiragana) {
Utils.storeWordBySuffix(appState, question);
} else {
Utils.storeKana(appState, question);
}

So, it's a little complicated to test because the words are stored in a list array, which is in turn stored in a hash table whose key is the word's suffix. To properly test this, we'll need to get the trailing hiragana, and then access the table by that suffix, then search through the the ensuing list to find the word.

This also leads to the question of test dependencies. If the previous test passes, all the data will be in the word table. But what if the dependent test runs first?

I just read something about how tests shouldn't be dependent. But - how do I know the word table has or hasn't been cleared? Maybe I should just clear it at the start of the test. Or is the whole app restarted for each test? The display does blink on and off when it's running.

Also, I'm wondering if there's a way for duplicate entries to slip in there when they restart the app. The suffix won't be, but it's just adding to a list. If the initialize runs more then once, there could be dups. I should add a test for that.

Well, first things first. Let's code the test to see if it gets there in the first place.

Ok, well, I need to test the suffix first. I'll add that to the Utils class:

public void testGetTrailingHiragana() {

String suffix = Utils.getTrailingHiragana("言う");
Assert.assertEquals("う", suffix);
}

Ok, that passed - but a couple of others failed!

testDatabaseMatchesView and testBlankKanjiIsHandled, on (what else is new) - null pointer exception.

The trace shows they are going into the word grouping. So, clearly my new code somehow broke something.

Let's see what happens if we back out the call to the SQL method, and re-implement the word setup in the original initialize.

Right - they all pass.


Ok, I see the problem. My code for the for the words needs to iterate through the returned list array. Previously, it had been the sql loop. Now it's getting the whole list in one go and needs to loop through them.


for (Question question : rows)
if (question.kanji != question.hiragana) {
Utils.storeWordBySuffix(appState, question);
} else {
Utils.storeKana(appState, question);
}


But the same tests still fail.

Well - what are my options? Debug or plow forward with the word test?

Let's plow forward with the test. It will isolate the code we need to look at.

Let's first check that the word table gets initialized...

public void testInitializeWordHashIntialized() {

AppState appState = new AppState();

setApplication(appState);

Intent intent = new Intent(getInstrumentation().getTargetContext(),
JlptQuizStartActivity.class);


JlptQuizStartActivity a = startActivity(intent, null, null);

InitUtils.initializeQuestionsSQL(a, appState);

Assert.assertNotNull(appState.wordGroupingsBySuffix);
}

Well, of course it doesn't pass.

Although the is supposed to allow me to mock the Application object, it's very possible that the running app is using a different app state. I'll have to track that down.

Well, the trace certainly shows plenty of calls to the store methods.

Try this:

appState = (AppState) a.getApplication();
Assert.assertNotNull(appState.wordGroupingsBySuffix);

Nope. Ok, I see the problem. The words are only initialized for a particular level. I'm testing at two different level. No - wait - why was it passing before?

Well, I think I need to call the initializeSQL from the other tests. No, that should be being called.

Just in case, I'll substitute in level 5, number 16 あそこ on "testBlankKanjiIsHandled".

Run it - nope. It never works until you understand the problem.

Ok, I see - those two tests actually run the initialize questions. When I checked for where they were called, I only called the checked the target - not the tests!

Ok, they pass now. Onward ho!