Saturday, May 30, 2009
a satisfied customer
Also, don't be a total dummkopf like me and name your initial test file "unittest.py". Insert headdesk sounds here. Fortunately I realized the problem after a mere five minutes of staring at bewildering error messages.
Labels: AppEngine, gaeunit, python, testing
Friday, May 29, 2009
Ajax, king of Salamis, son of Telamon and Periboea. No, wait. That other Ajax.
(The basic information stream for the app, at least initially, is "Android uploads GPS data, some brief notes, and maybe a picture to the middleware; middleware emails reminder to user to do something with it; user subsequently logs in to web site, conceivably from Android's browser, and uses previously uploaded data to update WikiTravel." I'm gonna add a more direct route that doesn't involve the web site for simple updates, but 'remind me to write this up in more detail later' seems like a more useful function, thanks largely to phone screen sizes.)
Anyway, I stopped writing code just as Ajax - that's Asynchronous Java/XML, more or less, although it has a highly fungible definition - began to take over the web world and begat Web 2.0. So as a mere user, I've always been pretty impressed by it. Saving and loading data within the browser? Cool! Must be tricky!
It ain't. On the contrary, it's really easy. Oh, I can see how you could tangle yourself up in knots with it, but for basic "go send stuff to server / revamp this piece of this page" stuff, Ajax is totally straightforward. Looks like it's often far simpler than doing everything on the server, even.
I did solve one microproblem, so I'll post some code. Ajax is built around an XMLHttpRequest object, which you basically instruct to "go send this request to the server, then get back to me." You then set a callback function XMLHttpRequest uses to get back to you. But I wanted to pass a parameter to my callback function. I wasn't clear on the best way to do this, so I googled, but while there were a few solutions out there, frankly, I think this one I came up with is more elegant. (And I'm sure I'm far, far from the first to work this out.)
Basically, you use the fact that JavaScript treats functions as objects, and write a function to create your callback function on the fly. Sound complicated? It really isn't. Here's JavaScript code to update/expand div with new data from the server:
var http;
function getResponseFunction(divName) {
return function() {
if (http.readyState==4) { //4 means "done!"
var element = document.getElementById(divName);
element.innerHTML = http.responseText;
element.style.display='block';
}
}
}
function fillDiv(divName) {
var element = document.getElementById(divName);
http = new XMLHttpRequest();
http.onreadystatechange=getResponseFunction(divName); // customized callback function
var targetUrl="/expandDiv?divName="+divName; // a call to this URL will return the HTML with which we'll fill the div
http.open ("GET", targetUrl, true);
http.send(null);
}
Voila. Piece o' cake.
(Yes, this project has now expanded to include Java, Python, and JavaScript/DHTML/Ajax code. Hey, it keeps me from getting bored.)
Labels: ajax, callback, JavaScript, XMLHttpRequest
Wednesday, May 27, 2009
On cloud computing nine. Well, maybe three.
AppEngine is remarkably easy to work with. And as my friend Martin pointed out, it's now available in both Java and Python (although the Java is still in "Early Look" status.) I'm working in Python for the sake of variety, and also because the advantages of Java in this context are not immediately obvious.
Anyway, I don't have as much Python experience as Java, but that hardly matters, because it's all very straightforward. I'm having a permissions problem getting the location of an IFrame, which means I may have to make things a little more annoying for the user than I'd like, but that's an XSS browser-security issue not a development issue.
The basics of a web application - getting data from the database and request, displaying it to the page, and saving it as and when needed - are all perfectly straightforward; so much so that I'm not even going to bother posting any code here, for once, because none of it seems particularly interesting. Which is a good thing. It means can focus on what you want to do, unlike the bad old days, where you spent a hefty fraction of your time worrying about how you're going to do it.
(I'm sure I'll hit such a wall at some point, and fear not, when I do I will whine about it at logorrheic length.)
AppEngine also gives you lots of freebies. Sending emails easily, for once. Scalability and data integrity, for two. Goodbye, J2EE deployment descriptors; that heavy lifting now happens pretty much behind the scenes, although you do have to group objects affected by single database transactions together in advance.
You also get automatic seamless user handling, so long as you use Google Accounts as your userbase. Caveat; it's easy to link to a login page, but I haven't quite worked out how to integrate a login form into your own pages. Even so, this is pretty brilliant. It means your site comes with all the user headaches - login, logout, password reminder, sending them emails, etc. - pre-handled, saving you time and grief. It also means that if you use it, which I am for the sake of convenience, you lock yourself even further into Google's infrastructure, and expand the tentacular remit of Google Accounts. Good thing they're not evil, eh?
Labels: AppEngine, Google, Java, python
Monday, May 25, 2009
I have been a good developer and deserve a cookie
See my very first post to see how to run these kinds of tests. It's kinda cool; after triggering 'em from the command line, you can flip to the Android emulator, and see the windows and text appearing and disappearing...
A few interesting points here, anyways:
- Note that my test code is in a different package from my app code.
- In related news, we call getInstrumentation().getTargetContext() to open our database, not, repeat not, getInstrumentation().getContext().
- And to call the button-push in "testSaveData()", we have to annotate that method as a "@UiThreadTest" method.
Feast thine eyes:
package com.rezendi.wtw.test;
import android.content.Intent;
import android.database.Cursor;
import android.test.ActivityInstrumentationTestCase2;
import android.test.UiThreadTest;
import android.widget.Button;
import android.widget.EditText;
import com.rezendi.wtw.R;
import com.rezendi.wtw.Settings;
import com.rezendi.wtw.TravelNoteEdit;
import com.rezendi.wtw.TravelNotesDbAdapter;
/**
* Test code for a travel-note lifecycle.
*
* @author Jon Evans
*
*/
public class NoteTest extends ActivityInstrumentationTestCase2{
public NoteTest() {
super("com.rezendi.wtw", TravelNoteEdit.class);
}
/* (non-Javadoc)
* @see android.test.AndroidTestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
}
/* (non-Javadoc)
* @see android.test.AndroidTestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
}
/**
* Open an empty TravelNoteEdit and check all is well.
*/
public void test1EmptyNote() {
TravelNoteEdit tne = (TravelNoteEdit) getActivity();
EditText titleEdit = (EditText) tne.findViewById(R.id.title);
String titleText = titleEdit.getText().toString();
assertEquals(0, titleText.length());
EditText bodyEdit = (EditText) tne.findViewById(R.id.body);
String bodyText = bodyEdit.getText().toString();
assertEquals(0, bodyText.length());
Button picButton = (Button) tne.findViewById(R.id.addPicture);
assertEquals(picButton.getText(), tne.getText(R.string.addPicture));
}
/**
* Open TravelNoteEdit, edit its contents, save its data, check saved data, delete.
*/
@UiThreadTest
public void test2DataSave() {
TravelNoteEdit tne = (TravelNoteEdit) getActivity();
tne.setLocationString("wtwTest"); // mark this as test data
EditText titleEdit = (EditText) tne.findViewById(R.id.title);
titleEdit.setText(titleEdit.getText().toString()+ "test edited");
EditText bodyEdit = (EditText) tne.findViewById(R.id.body);
bodyEdit.setText(bodyEdit.getText().toString()+ "test edited");
Button saveButton = (Button) tne.findViewById(R.id.save);
saveButton.performClick();
//OK, we're done, so;
TravelNotesDbAdapter dbAdapter = new TravelNotesDbAdapter(getInstrumentation().getTargetContext());
dbAdapter.open();
Cursor note = dbAdapter.fetchTestNotes();
String newTitleText = note.getString(
note.getColumnIndexOrThrow(TravelNotesDbAdapter.KEY_TITLE));
String newBodyText = note.getString(
note.getColumnIndexOrThrow(TravelNotesDbAdapter.KEY_BODY));
note.close();
assertEquals("test edited", newTitleText);
assertEquals("test edited", newBodyText);
dbAdapter.deleteTestData();
dbAdapter.close();
}
/**
* Create a note in the database, open TravelNoteEdit, check its contents.
*/
public void test3DataLoad() {
TravelNotesDbAdapter dbAdapter = new TravelNotesDbAdapter(getInstrumentation().getTargetContext());
dbAdapter.open();
long testId = dbAdapter.createNote(Settings.KEY_TEST, "test title", "test body", "test picture");
Intent intent = new Intent(getInstrumentation().getTargetContext(), TravelNoteEdit.class);
intent.putExtra(TravelNotesDbAdapter.KEY_ROWID, testId);
this.setActivityIntent(intent);
TravelNoteEdit tne = (TravelNoteEdit) getActivity();
EditText titleEdit = (EditText) tne.findViewById(R.id.title);
String titleText = titleEdit.getText().toString();
assertEquals("test title", titleText);
EditText bodyEdit = (EditText) tne.findViewById(R.id.body);
String bodyText = bodyEdit.getText().toString();
assertEquals("test body", bodyText);
Button picButton = (Button) tne.findViewById(R.id.addPicture);
assertEquals(picButton.getText(), tne.getText(R.string.seePicture));
dbAdapter.deleteTestData();
dbAdapter.close();
}
}
Labels: Android, InstrumentationTestCase, Java, JUnit, testing
Sunday, May 24, 2009
Background tasks and upfront dialogs
- the pre-Cupcake (Android SDK 1.5) way of using Handlers, which works, but is not exactly intuitive and easy to follow;
- the far preferable Cupcake way, of creating and executing inner class that overrides the AsyncTask class, which runs a resource-intensive thread in the background, provides progress updates, and reports back when finished.
Here's the AsyncTask I wrote today:
private class UploadNoteTask extends AsyncTask{
protected Boolean doInBackground(Long... longs) {
//create progress dialog
publishProgress(1);
//we can't do this in the target because it's not an Activity
SharedPreferences mySettings = getSharedPreferences(Util.AppName, 0);
String currentEmail = mySettings.getString(Settings.KEY_EMAIL, "");
//try to upload the note; longs[0] is the row ID
boolean noteUploaded = mDbHelper.uploadNote(longs[0], currentEmail);
publishProgress(100);
return new Boolean(noteUploaded);
}
protected void onProgressUpdate(Integer... progress) {
showProgress("Uploading...", "Uploading data", progress[0]);
}
protected void onPostExecute(Boolean wasSuccessful) {
//get rid of the progress bar
killProgress();
//tell the user how it went
if (wasSuccessful.booleanValue()) {
showDialog("Success", "Note uploaded");
}
else { // warn user of failure
showDialog("Failure", "Could not open note: error message "
+mDbHelper.getUploadError()
+". Try again later.");
}
}
}
(Yes, obviously I'm going to move all those hardcoded strings into R.strings very soon, improve the error handling, etc.)
And here's how you call it:
new UploadNoteTask().execute(mRowId);
which I think we can all agree is not too onerous.
I have, however, run into an annoying problem. It's convenient to run this as an inner class in your Activity, as you can then call Activity-inherited methods such as the "getSharedPreferences()" method up above. However, I want to be able to upload a note both from the list of notes and the detail page for a particular note - which means I have to either
- duplicate the above code in both Activity classes (ugly!)
- not use any Activity code in my AsyncTask (limiting!) and/or instead pass an Activity or Context in as part of its input Params, which will therefore then need to be Objects and cast down from there (reeeeeally ugly!)
So for the moment I'm holding my nose and duplicating code. (I can't put it in a common superclass because one of my Activities inherits from ListActivity, and the other does not.) Not just the class code, either, but the "showDialog" "showProgress" and "killProgress" methods, which tell the user what's happening/happened as you upload.
At least these kinds of pop-up dialogs are very easy to work with. Here are those three methods just cited, in their entirety:
private void showDialog(String title, String message) {
AlertDialog ad = new AlertDialog.Builder(this).create();
ad.setButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
return;
} });
ad.setTitle(title);
ad.setMessage(message);
ad.show();
}
private void showProgress(String title, String message, int progress) {
if (mProgress==null) {
mProgress=ProgressDialog.show(this, title, message);
}
mProgress.setProgress(progress);
}
private void killProgress() {
if (mProgress!=null) {
mProgress.dismiss();
mProgress=null;
}
}
Still, my cavil above aside, I must admit it's a nice and easy way to handle background processing in a separate thread, while keeping the user semi-informed.
Labels: Android, AsyncTask, dialogs, Java, multithreading
Friday, May 22, 2009
Well, that was easy.
I expected the debugging to be messy and lengthy. But whaddaya know? All I had to do was add the INTERNET permission to my AndroidManifest.xml, and correct the URL that I was pointing to (I'm running the AppEngine app locally; the Android emulator has a special IP address, 10.0.2.2, to connect to its host machine) and poof, amazingly, It Just Worked.
Here's the Android code, in case anyone needs an example:
public boolean uploadNote(long rowId, String location, String title, String comments, String picturePath) {
try {
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getParams().setParameter("http.useragent", Util.AppName);
HttpPost httpost = new HttpPost(Util.wtwSite); // temporarily 10.0.2.2:8080/sendUpdate
Log.i(""+this, "Preparing to post to "+Util.wtwSite);
ListparamList = new LinkedList ();
paramList.add(new BasicNameValuePair("email", Util.GetUserEmail()));
paramList.add(new BasicNameValuePair("title", title));
paramList.add(new BasicNameValuePair("location", location));
paramList.add(new BasicNameValuePair("comments", comments));
paramList.add(new BasicNameValuePair("image", picturePath));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList,
HTTP.DEFAULT_CONTENT_CHARSET);
httpost.setEntity(entity);
HttpResponse response = httpclient.execute(httpost);
Log.i(""+this, "Sent POST, got " + response.getStatusLine());
entity.consumeContent();
markUploaded(rowId);
return true;
}
catch (IOException ex)
{
Log.e(""+this, "Could not upload note with row "+rowId+ " due to "+ex, ex);
return false;
}
}
I'm not even going to bother posting the server code, as it's so simple; 70 lines of Python, and 30 lines of HTML.
You'll note at the moment I just upload a picture URL, rather than actual data, but I'm going to move to doing the latter eventually.
Labels: Android, AndroidManifest, AppEngine, Java, python, upload
Thursday, May 21, 2009
From Android to AppEngine
Why a web app? Well. My Android app basically consists of grabbing the phone's current location, adding a picture and notes if the user so desires, and uploading this data to a web site. (I'm actually thinking of hiving off this basic functionality as a "Scout API" down the line, and releasing the code for future Android novices; I'm guessing that "hey, let's have people register / comment on the locations of $things1 when they come across them!" apps out there.
But a phone has very limited resources, from CPU to storage, and an annoying UI; so rather than make the user do everything on the phone, far better just to grab the on-the-spot data and do whatever further processing is required on a web site far, far away.
So I've started playing with Google AppEngine. Thus far it looks very easy, very powerful, and integrates seamlessly with Google Accounts, so you don't have to roll your own user infrastructure (and so you get locked into theirs....) It's also currently only available in Python, but what the hell, I was getting bored of Java anyway, right? More on AppEngine in the next post.
Other things I learned about Android:
Don't tug on SurfaceView's cape
The camera API is surprisingly simple, and Android comes with a SurfaceView which, among other things, lets you easily display a real-time camera preview (if you subclass it.) My intent was to have the "take picture" and "see preview" user actions be part of the same Activity - ie, when the user selects "take picture", stop showing the preview, and instead draw that snapshot on the SurfaceView.
This did not work well. SurfaceView is a very weird artifact and a difficult one to play with. After about two hours of beating my head against the problem I wisely decided to create a "ShowPicture" Activity with an ImageView, which was much simpler, and worked much better. Moral: don't overload any given Activity. Keep 'em simple, stupid.
Also, because I subclassed SurfaceView into CameraView (as per the Camera example in the API Demos that come with the SDK) I had to have a two-level callback structure: in my Camera Activity, there was a TakePicture button, which called "takePicture()" in my CameraView, which instructed its Camera to snap a shot. Then there were three callbacks in CameraView (one to indicate success, one for the RAW, one for the JPEG) and in turn I had to callback Camera. This works fine, but is a little more convoluted than I'd like.
Be careful about where you are in the stack
Creating a new ShowPicture activity did create a new problem, in that the user navigation might be "edit note -> take picture -> see preview" and might be "edit note -> see existing picture -> take new picture". In the first case, after you take the picture, you want to open a new SeePicture Activity; in the second, you want to return to the existing one. The solution was to pass a flag in the Intent that goes to the Camera activity, indicating whether you're taking a new picture or replacing an existing one.
Always save when leaving a window
In theory, Android could kill your app at any time, to conserve resources. It will probably notify you before it does so (see the Activity Life Cycle.) So it's best to save all user-modified information in "onPause()", just on case. This incentivizes you to keep your database structures very simple.
This does create the cancellation problem as per my last post, but I think that's the lesser of two evils.
GPS is busted in the emulator
The emulator's mock location providers basically don't work. Fortunately, this basically doesn't matter, as you can roll your own if you really care.
1For the record, I hate Perl with an abiding passion, but the $ prefix remains the best way to signify "this is a variable" in prose.
Labels: Android, AppEngine, CanvasView, Java, SurfaceView
Saturday, May 16, 2009
Things I learned about Android today
- To use either camera or location data in a package , you have to add a permission entry to its AndroidManifest.xml.
- Also, when you add an Activity, don't forget to add it to the manifest.
- "this" in an anonymous inner class refers, of course, to the inner class. Duh.
- I've stumbled into an interesting problem with cancellation. Because any Android activity can be preempted and/or killed at any time, you have to be ready to save the current state. But in order to provide a cancel button, you also have to save the initial state. Which may mean, if you're using a database for storage, that you have to have two DB rows allocated to the current UI. And bear in mind that more than one copy of your Activity might be running at any given time ... I have some notion how I might deal with this, but am hoping some more elegant solution will arise.
- The XML-file layout structure is a bit clunky, but both powerful and flexible.
- That said, I still haven't worked out how to make a ListView's entries selectable across their entire width, rather than just on their text field. (And woe betide you if that text field is empty ...)
- To create and use a custom View subclass, you have to add a new xml namespace to the layout file, and provide all the necessary constructors. (And, um, it's also important to spell its package name correctly.)
- The camera is remarkably easy to work with. So far.
- Location information is, too, but it doesn't seem to sit well with the emulator; I've tried two different ways to simulate a particular location, but am still getting null back when I ask the emulator where it is.
- Running test cases in the emulator's context works pretty well. Debugger, too.
OK, beer beckons, more next week.
Labels: Android, AndroidManifest, cancellation
Friday, May 15, 2009
Pronoid Android
My name's Jon Evans. I'm a novelist, journalist, comics scriptwriter, and lapsed former software engineer - see my web site rezendi.com for far more than you ever wanted to know. This is a blog about my (mis)adventures in Android development, and related subjects.
I used to be a fairly expert software developer, with an EE degree from Canada's University of Waterloo, and years spent as a coder and project lead at various consulting companies in California, London, New York, and Toronto. Then I took six years off to write books. Now that I'm jumping back in the saddle (currently for fun, eventually for profit) I thought that documenting what I learn en route might be useful to both myself and others.
So, let's begin at
The Beginning
You need to download three things to start working with Android: the latest Java Software Development Kit (SDK) the Eclipse Integrated Development Environment (IDE), and the Android SDK itself. Technically, you could live without the Eclipse IDE, but trust me, it makes your life a lot easier.
The downloads were pretty straightfoward. Unpacking the zip files took forever, as in several hours. I suppose this is what I get for having Windows Vista on my development machine. It has already become apparent that I need a faster computer; my two-year-old cheapest-Dell-that-money-could-buy is still perfectly adequate for word processing and web surfing, but software development is a little more demanding.
Hello, Android!
The Android SDK comes with decent getting-started documentation, including a detailed "Hello world!" howto. A longstanding software tradition dictates that the first thing one does with any new development platform is make it output "Hello, world!" to your screen; and a longstanding truism warns that if this is hard to do, it means your new platform is complex and difficult to work with.
Fortunately, Android was pretty straightforward, at least in Eclipse, though I suspect without the IDE I would have been muttering and cursing in fairly short order. First you have to create the "virtual device" on which your code will run; basically, a software emulator of an Android phone. Then you create an Eclipse project, and four files: HelloWorld.java (five lines of code), AndroidManifest.xml (packaging information), main.xml (UI information) and strings.xml (what it says on the box.) When you run them, a very impressive emulated Android phone pops up on your screen, and when you push its MENU button to unlock it, voila, "Hello, Android."
Two notes. One is an Eclipse bug; it sometimes hangs when launching the emulator, without ever installing your package and/or turning over control of the virtual phone. This seems to be correlated with adding or removing Eclipse projects beforehand. (Note, however, that you can and should re-install packages into a running emulator.)
The other is that I had to go into the filesystem and remove a bunch of source files that began with "._" for the app to work. This was true for the Notepad tutorial files as well.
Oh, yes. The tutorial. "Hello world!" is useful, but doesn't actually teach you all that much, especially in an environment where (so far, at least) you mostly write connective tissue for Android's extensive existing capabilities, rather than creating things from scratch. So the SDK comes with a three-step Notepad tutorial to introduce you to its class hierarchy, application lifecycle, and object library. I was impressed by how much you can do with just a few lines of code.
Testing, Testing
Like all good software engineers, I intend to do Test-Driven Development. (Well, sorta. Full-on TDD means you write the tests first, then the code; I'm going to cheat a little and do "code a little, test a little, code a little, test a little" instead.) In theory, Android comes with built-in out-of-the-box unit-test capabilities that makes this very straightforward.
The practice is a little more challenging. Not least because the "API Demos" part of the SDK failed to install on my machine - the documentation wasn't created, and the classes won't compile, for no obvious reason. The source code remains accessible, but there's still no succinct explanation of how to create Android test cases, much less a full-fledged Android test harness.
So I spent an hour or two today trial-and-erroring my way through that obstacle course, and am pleased to say that I now have Android unit tests working both from within Eclipse and from the command line. I was helped greatly by this blog post and those it links to. Here's my own attempt to summarize:
- Create an Eclipse project with your working code. (In my case, the project is WTW, and the java package is "com.rezendi.wtw").
- Create an Eclipse project where test code will go. (In my case, WTWTest, java package "com.rezendi.wtw.test").
- Write a test case that inherits from android.test.AndroidTestCase, and create "testXYZ()" methods in it. (I haven't yet written tests that use the emulated phone's Context; that's the next step. But I presume since basic tests work on the emulated phone, the more complex ones will too.)
- Modify the AndroidManifest.xml in the test project to point to an InstrumentationTestRunner. Something like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rezendi.wtw.test"
android:versionCode="1"
android:versionName="1.0">
<application>
<uses-library android:name="android.test.runner">
</uses-library>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetpackage="com.rezendi.wtw" android:label="Tests for WTW.">
</instrumentation>
</application>
</manifest> - To run it from Eclipse, right-click on (I think) either WTW or WTWTest, and select "Run As -> Android JUnit Test." The emulator will launch. Once it has, select the phone's app list (that upward-arrow button from the home menu), select "Dev Tools", select "Instrumentation", and then select your package (in this case, "WTW".) JUnit output should go to your Eclipse console.
- If you find this ridiculously complex and time-consuming - and you should, because forcing the user to jump through all those UI hoops to run tests that should be automatable is ridiculous - you can do the same thing through the command line. Run your test package normally, ie as an Android Application. Ignore the "no Launcher activity found" warning. The package being tested will also be installed into the emulator.
- Once the emulator is up and running, open a command line, go to the "tools" subdirectory in your Android install directory, and use the "adb" tool to connect to the emulation and run your test cases. In my case, the correct command line is: "adb shell am instrument -w com.rezendi.wtw.test/android.test.InstrumentationTestRunner".
- Profit!
Labels: Android, AndroidManifest, Eclipse, InstrumentationTestRunner, Java, testing
Subscribe to Posts [Atom]