Tuesday, April 6, 2010

 

SQLite on Android

So a commenter on Android Market suggested I allow users store my app data on their SD card rather than phone memory. Great idea, I thought, and promptly went Googling to look for some example code. Alas, there wasn't any. So I wrote my own; and here it is for those who find themselves in a similar situation.

The basic Android model is for all Activity classes to have an associated DBHelper class. I don't like this at all; you waste all kinds of time writing and instantiating and opening and closing your DBHelpers, for the sake of (in my app) maybe twenty different database calls. So instead I went and created a singleton DB object. Here's the static code:


public class DB {
private static DB instance;
SQLiteDatabase mDB;
DatabaseHelper mDbHelper;
final Context mCtx;

public static DB GetFor(Context context) {
if (instance==null)
instance = new DB(context);
if (!instance.isOpen())
instance.open();
return instance;
}

public static void Close() {
if (instance!=null && instance.isOpen())
instance.close();
instance=null;
}

/**
* Database creation sql statement
*/
static final String MESSAGE_CREATE =
"create table Message (_id integer primary key autoincrement, "
+ "content text not null, dateCreated datetime not null, dateViewed datetime, "
+ "messageTitle text, messageType integer, sender text, recipient text); ";

// [...other table definitions go here...]

static final String DATABASE_NAME = "iTravel";
static final int DATABASE_VERSION = 1;


(I call DB.Close() in the onDestroy() method of the app's Activities. Now, if you had multiple threads writing to the database simultaneously, this approach would probably get pretty messy in a hurry; fortunately, I don't.)

The Android SDK also includes a SQLiteOpenHelper object, which basically takes a Context object and returns a writeable database. I was a little irritated by the need to pass in a Context - it means you have a Context on hand to access the database at all - but presumed it was just necessary for some mysterious reason.

Not so mysterious at all. The bad news is, if you want your database to live on your phone's SD card, you can't use the SQLiteOpenHelper. The good news is, it's easy to write an SD-card-compatible variant, one that doesn't require any Context at all:


static class DatabaseHelper{
private SQLiteDatabase db;
private Context mCtx;

private DatabaseHelper(Context context) {
mCtx=context;
}

public void open() {
File dbDir=null, dbFile=null;
if (Settings.DoSDDB() && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
dbDir = Environment.getExternalStorageDirectory();
dbFile = new File(dbDir, "iTravel.sqlite");
}
else
dbFile = mCtx.getDatabasePath("iTravel");

if (dbFile.exists()) {
Log.i("SQLiteHelper", "Opening database at "+dbFile);
db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
if (DATABASE_VERSION > db.getVersion())
upgrade();
}
else {
Log.i("SQLiteHelper", "Creating database at "+dbFile);
db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
create();
}
}

public void close() {
db.close();
}

public void create() {
Log.w(""+this, "Creating Database "+db.getPath());
db.execSQL(MESSAGE_CREATE);
// ...other tables go here
db.setVersion(DATABASE_VERSION);
}

public void upgrade() {
Log.w(""+this, "Upgrading database "+db.getPath() +" from version " + db.getVersion() + " to "
+ DATABASE_VERSION + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS Message");
// ...other tables go here
create();
}

public SQLiteDatabase getWritableDatabase() {
if (db==null)
open();
return db;
}


Turns out the only thing we ever needed that Context for was its "getDatabasePath()" method, as that varies by application; but if you're using the SD card, you don't need that at all. (The above implementation supports both SD-card and phone-memory databases. Just in case.)

So what happens in the DB object's instance(s)? Easy enough -

public DB(Context ctx) {
this.mCtx = ctx;
}

public DB open() throws SQLException {
if (mDbHelper==null)
mDbHelper = new DatabaseHelper(mCtx);
if (mDB==null || !mDB.isOpen())
mDB = mDbHelper.getWritableDatabase();
return this;
}

public void close() {
mDbHelper.close();
}

public boolean isOpen() {
return mDB!=null && mDB.isOpen();
}

public boolean deleteAll() {
mDB.delete("Message", null, null);
// ...other tables go here
return true;
}

public Message[] fetchMessages(String where, String[] args) throws SQLException {
Cursor cursor = mDB.query(true, "Message",
new String[] {"_id", "messageTitle", "content" },
where, args, null, null, "dateCreated desc", null);

Message[] messages = new Message[cursor.getCount()];
cursor.moveToFirst();
int i=0;
while (!cursor.isAfterLast()) {
messages[i++] = new Message(cursor.getString(1), cursor.getString(2));
cursor.moveToNext();
}
cursor.close();
return messages;
}

public void saveMessage(Message message) {
ContentValues values = new ContentValues();
values.put("messageTitle", message.getTitle());
values.put("content", message.getData());
values.put("messageType", message.getMessageType());
values.put("dateCreated", System.currentTimeMillis());
mDB.insert("Message", null, values);
}


et voila: all your database access in one place, accessed by a simple DB.GetFor(Context context) call from anywhere in your app (and you don't even need the Context if you know the DB will live on the SD card!) and nicely abstracting out the crazy ten-positional-arguments-in-a-row "query" method on SQLiteDatabase.

Labels: , , , , , , , , , , ,


Monday, March 1, 2010

 

Responding to zooms and pans in Android's MapView

There's a curious lacuna in Android's MapView API: it's (relatively) easy to display a map, and overlay items atop a map; it's (relatively) easy to detect when an overlay item has been touched; but there's no straightforward way to work out when the user has touched anywhere else on the map. Because of the way MapView works, the usual onTouchListener() solution only works for one touch, and then fails.

There is, however, a solution. Not a pretty one, but it works. The solution is to use your own subclass of MapView:


public class ITRMapView extends MapView {
public ITRMapView(android.content.Context context, android.util.AttributeSet attrs) {
super(context, attrs);
}

public ITRMapView(android.content.Context context, android.util.AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public ITRMapView(android.content.Context context, java.lang.String apiKey) {
super(context, apiKey);
}
}


and within that subclass, override onTouchEvent. You're probably only interested in the UP action:

public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction()==MotionEvent.ACTION_UP) {
//do your thing
}
return super.onTouchEvent(ev);
}


But what if you want to detect zoom actions as well as touches? Simple, thought I: just override onDraw() and do the same thing. But you can't - MapView's onDraw() is final!

Catastrophe? Not quite. It turns out that overriding dispatchDraw() works just fine:

int oldZoomLevel=-1;

public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getZoomLevel() != oldZoomLevel) {
//do your thing
oldZoomLevel = getZoomLevel();
}
}


Et voila - you can programmatically detect and respond when the user pans and/or zooms your map. Really not so hard after all.

Labels: , , , , , , , , , ,


Monday, February 8, 2010

 

Android vs. iPhone: A Developer's Perspective, part II

Android vs. iPhone: A Programmer's Perspective - II


(See also Part I.)


OS features

The big winner here is Android, due to its multitasking. You can call other apps, eg a browser or a file-system picker, get results from them, and use those results in your own app; all these things are completely impossible on the iPhone, where (aside from a few special cases like iTunes) only one app may run at a time. (And even if you want to sacrifice yourself to launch another app, you can only do so in very restricted circumstances - if they have registered a URL scheme.)

The browser special-case is worth mentioning; you can include browser windows in your own app, and mine do so in a few different places - but this is often not as full-featured or convenient to the user as opening up the full Safari browser.

You can also write Android services that run in the background, and you can launch them at boot time (if the user permits) by registering for the BOOT_COMPLETED intent. What it doesn't really, provide, though, is "push" notifications (other than by faking it with eg Comet HTTP Push) which the iPhone does offer. That's the iPhone's only advantage in this category, though.


Phone features

The iPhone is locked down. You don't get direct Bluetooth API access; you do in Android. You don't get direct SMS access; you do in Android. For security reasons, they claim in Cupertino. At least (like Android) it now lets you access the proximity sensor.

But to its credit, the features it does offer are a joy to work with. In particular, the built-in camera/preview screen (for iPhone) / picture selector (for iPod Touch) is excellent, and requires all of a half-dozen lines of code to launch and respond to. The Android camera code, last I looked at it, was much more complex.

Location management is a little messy and complex on both systems, but overall Android's registration model is easier to work with than the iPhone's delegation model.

The iPhone SDK comes with this daft notion that all settings for all apps should probably be in a single System Settings screen accessible from the main menu; you can roll your own, but it's inconvenient. Android, by contrast, lets you create a settings screen by simply writing XML, no Java required unless you want to customize it. On the other hand, the iPhone simply makes "your default settings" available, whereas Android provides the possibility of multiple sets, which is doubtless more flexible and powerful but also more annoying to work with.

Accessing system and app resources (eg image files) is a little counterintuitive on Android; at compile time, it scans a predetermined bunch of directories, and automatically builds an "R" file with a bunch of final static ints, each of which uniquely identifies a resource; you then use those in code to access resources. (There's also an Android.R for built-in-resources.) This is confusing at first, but fine once you get used to it.

The iPhone makes the basics easy for you in code -
[Image imageNamed:@"myImage.png"]
- but if you want to go beyond that, the whole resource-bundling thing is less than intuitive, and while I had no trouble accessing bundle resources, I never felt like I had a clear idea of what was actually going on, unlike with Android.


Screen building

No sense pussyfooting around: when it comes to actually building the screens of your app, the iPhone has a massive advantage. It provides an excellent WYSIWYG tool, and the components it offers - buttons, lists, etc - mostly just look a whole lot nicer and sexier than the Android ones.

(That said, I have two minor complaints about the iPhone UI components: 1) No drop-down options in menus - instead you either have to code an ActionView or use one of the huge screen-eating spinners. 2) The absence of a border around TextViews does not look good and just serves to confuse users.)

In general, though, the iPhone wins here. The delegate model of TableViewController gives you powerful and fine-grained control far more easily than the adapter model of ListActivity. As far as "complicated, hard-to-work-with, but useful subclasses of your basic list view" goes, I'll take the iPhone's LocalizedIndexedCollation over Android's ExpandableListView, though I sure wish both were more developer-friendly.

And then there's maps. Jeez. In iPhone, if you want to add a custom marker for a map, then in the ViewController for that screen, you just override a method and write six lines of code:

- (MKAnnotationView *)mapView:(MKMapView *)myMapView viewForAnnotation:(id )annotation {
NSString viewImageName=@"myImage.jpg";
MKAnnotationView *myView = (MKAnnotationView*)[myMapView dequeueReusableAnnotationViewWithIdentifier:viewImageName];
if (myView==nil) {
myView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:viewImageName] autorelease];
myView.image = [UIImage imageNamed:viewImageName];
}
return myView;
}


In Android, first you have to write a whole new inner class that extends ItemizedOverlay, eg:

class ListingsOverlay extends ItemizedOverlay {
private ArrayList overlays=new ArrayList();

public ListingsOverlay(android.graphics.drawable.Drawable defaultMarker) {
super(defaultMarker);
}

public void addOverlay(OverlayItem overlay, Drawable marker) {
super.boundCenterBottom(marker);
overlay.setMarker(marker);
overlays.add(overlay);
}

// necessary because populate() is protected
public void doPopulate() {
populate();
}

@Override
protected OverlayItem createItem(int i) {
return(overlays.get(i));
}

@Override
public int size() {
return(overlays.size());
}


and then something like this:


mapView = (MapView) findViewById(R.id.mapview);
mapOverlays = mapView.getOverlays();
mapOverlays.removeAll(mapOverlays);

Drawable drawable = getResources().getDrawable(R.drawable.map_fave);
itemizedOverlay = new ListingsOverlay(drawable);
OverlayItem overlay = new OverlayItem(point, mappable.getIDString(), mappable.getMapDetailText());
Drawable marker = MappableItem.GetMarkerForMappable(ITRMapView.this, mappable.getCategory());
itemizedOverlay.addOverlay(overlay, marker);
itemizedOverlay.doPopulate();


Menus and navigation are also easier and prettier on the iPhone; you can add sleek-looking buttons and toolbars and simply set them to call the selector of your choice, and you get a great NavigationController for iTunes-like interfaces, plus sexy animation. No real equivalent on Android.

On the other hand, Android's XML layouts, while tedious and irritating, and not as pretty or near as exact as the iPhone's WYSIWYG, do work well once you get the hang of them - and they make it much easier to support multiple screen sizes and different orientations. (My iPhone app simply doesn't do landscape orientation; my Android app handles it almost perfectly, without me ever having thought about it.) This wasn't a big deal for the iPhone until last month - but apps that previously were confident of a 320x480 screen now have to deal with the iPad.


The Internet

Accessing web services and launching in-app web views is easy and effective in both Android and iPhone. Edge to the latter, though; there are a couple of weird little bugs with Android's WebViews (though they can be worked around with ease) and the iPhone gives you both more SDK options and better documentation.


Release

Building an iPhone app is kind of scary. You're suddenly reminded that under the hood it's terrifying C++; the "Build" screens are full of dozens if not hundreds of byzantine, cryptic, intimidating options for compiling, precompiling, linking, etc., and you find yourself desperately hoping you've set up your import libraries perfectly and suddenly very careful not to touch anything.

That said, XCode works really well. (Have I mentioned that debugging is far easier with the iPhone SDK? Debugging is far easier with the iPhone SDK. With Android I usually wind up resorting to debugging with log messages.) What does not work really well is Apple's paranoid certification hegemony. God forbid that anyone run an app without going through the App Store!

So you need to go to Apple's site and futz around with it and with device IDs and create and download separate certificates for debug and release, and your temporary "provisioning" device certificates expire every three months, and while it is theoretically possible to build an app for someone else's device, email it to them, and have them install it, I have yet to actually succeed at this, despite repeated attempts. (It's somewhat easier if their device is plugged in to your machine.)

You know how it works on Android?
- You build your app.
- You sign your app. (Which Eclipse can take care of with a simple wizard.)
- Anyone in the whole world who wants to can now download and run the app.

There's a slight pitfall if you're working with Google Maps - you have to jump through hoops like Apple's to create separate debug and release Maps API Keys, and ensure you're using the right one - but by and large, it's miles easier and better than trying to finagle your way into Apple's walled garden.

Plus, if you've built an app and released it, and found some sort of subtle bug? With Android, you can fix that and have a new version up on the Android Market in five minutes. With Apple, it's ... a week? A month? Who knows? App Store approval is an infuriating black box.


Overall

They're both excellent systems. They both have their pros and cons. Overall I would rate the iPhone as better, both in terms of what you can do with it and how - but Android is superior in fundamental ways (eg multitasking and memory management) and catching up fast in terms of results. If Apple doesn't watch out, and move fast, they're going to find themselves superseded soon. Maybe this year.

Labels: , , , , , , , , ,


Sunday, February 7, 2010

 

Android vs. iPhone: A Developer's Perspective, part I

Android vs. iPhone: A Programmer's Perspective


(See also Part II.)

I've spent the last month or so building both Android and iPhone versions of my app iTravel. (See http://wetravelright.com/ for details and links.) Which gives me a pretty good perspective from which to compare and contrast the Android and iPhone environments and SDKs. Hence I give you the following head-to-head analysis, from a developer's point of view:


Language

Non-programmers often think that one's language of choice is a big deal, but really, once you've learned two or three programming languages, picking up another is generally something you can do in a day or two. That said, there are often substantive differences. And this is definitely true of Java (for Android) and Objective-C (for iPhone.)

Let's start off with the really annoying stuff: Objective-C's memory management. By which I mean, its complete lack of any. Programmers have to manually allocate and release memory when writing for the iPhone SDK, which is positively medieval. If you fail to do so, and there are many pitfalls, then you leak memory which is lost until the device reboots. This is awful. (And it's no longer true of the Mac SDK, incidentally; but the iPhone is behind the times.)

There are other annoyances. You have two files to contend with for every class - a .h and a .c file. Which is inconvenient and complicating and inelegant. And suppose you have a basic, bog-standard instance variable. You generally wind up declaring it in, count 'em, not one, not two, not three, but four different places.

In your .h:

{
NSObject *object
}

@property (nonatomic, retain) NSObject *object


In your .c;

@synthesize object

and later, in dealloc(),

[object release];

Whereas with Java, you have one single .java file, which in general will have

private Object object;
public Object getObject() { return object; }
private void setObject(Object o) { object=o; }

all in one place. I know which one I think is easier.

But. On the other hand. Objective-C is sort of the bastard son of C, which is awful, and Smalltalk, which is awesome. As a result, it has Smalltalk-like features like selectors:

[caller performSelector:@selector(myFunctionName)]

...in short, you can use functions as (more or less, ish) first-class objects. Try that in Java and if memory serves you'll most likely wind up in the irritating labyrinth of reflection. Plus, you get options like "doesNotRecognizeSelector:", which can be easily misused, but is potentially very powerful, and does not exist in Java.

On the other hand, Objective C is really annoyingly logorrheic (meaning wordy) especially when it comes to string handlers. Suppose you have strings A and B, and you wish to combine them into string C. In Java, the syntax is

C=A+B;

whereas in Objective-C, you type

C = [A stringByAppendingString: B]

Or suppose you want the location of the last slash in string A. Java:

n=A.lastIndexOf("/");

Objective-C:

n = [A range ofString:@"/" options:NSBackwardSearch].location;

I much prefer Java's syntax and simplicity. But I do admire Objective-C's flexibility.


Development environment

By this I mean: the "integrated development environment" in which coders work; the documentation for the IDE, the language, and the libraries; the testing and source-code support; and all the stuff that is meant to help you write better code faster.

The Android and iPhone IDEs and documentation really incarnate the attitudes of the two companies in question. Android doesn't exactly have an IDE of its own, although they recommend that you use the Android plug-in for the open-source Eclipse IDE, which is slow, irritating, and buggy in various minor ways. The documentation is written by smart people for smart people, with little handholding. Flashy graphics are minimal to nonexistent. And a lot of important stuff is still handled by tools meant to be run from a shell rather than a GUI. But the search function is excellent.

Apple's IDE is slick, seamless and powerful. It comes with a visual tool to help you lay out the screens of your app. The documentation is full of step-by-step guides (although they are often oddly lacking or confusing) and high-quality graphics and other visualizations.

I have many complaints about both. Eclipse is slow and annoying, and I could only get Android's JUnit test harness to run successfully from a terminal window, rather than the IDE; similarly, I had to use shell tools to sign packages, get a fingerprint for a Maps IDE, install a package on my phone, etc. All of which really calls into question the I in IDE.

On the other hand, at last the external unit-testing actually works; XCode's built-in harness is so bad that Google built and released an entirely separate one, which has the advantage of actually functioning correctly. On the other hand, its Subversion integration is excellent, which is not true of Eclipse.

Both have plenty of official and unofficial online support, as well, at official support sites and places like Stack Overflow. Android's open-source ethos gives it a big advantage in terms of external packages, though; for instance, if you want to build a barcode scanner into an Android app, there's a whole open-source library out there for you, ready to be plugged in. For the iPhone? You'll have to roll your own. Sorry.


Multithreading

In phone apps multithreading is key, because the phone needs to remain as responsive as possible, so you need to do your heavy lifting behind the scenes, outside the main UI thread.

Both the iPhone SDK and Android support multithreading, but the latter's is much more convenient, especially if you want to call back to the main thread once your background thread has done its thing. On the iPhone, you have to do something like:

-(void) doWebView {
NSThread* htmlThread = [[NSThread alloc] initWithTarget:self selector:@selector(loadHtml) object:nil];
[htmlThread start];
[htmlThread release];
}

-(void) loadHtml {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *html = doUnroll ? [self getUnrolledData] : root.data;
html=[NSString stringWithFormat: @"%@%@</body></html>", [Util getBaseHtml], html];
[self performSelectorOnMainThread:@selector(setHtml:) withObject:html waitUntilDone:NO];
[pool release];
}

-(void) setHtml:(NSString*)html {
[webView loadHTMLString:html baseURL:nil];
[webView sizeToFit];
[self.tableView setTableHeaderView:webView];
}

Whereas on Android, you can use Java's equally irritating Runnable() framework, but Android provides an extremely convenient (and quite flexible) short form. Just subclass AsyncTask in an inner class:

new LoadHtmlTask().execute();

class LoadHtmlTask extends AsyncTask {
protected String doInBackground(String... strings) {
String headerData = Settings.GetHtmlPrefix() + (unrolled ? getUnrolledData(viewRoot) : viewRoot.getData());
return headerData;
}

protected void onPostExecute(String results) {
mHeaderView.loadDataWithBaseURL("local", headerData, "text/html", "utf-8", "");
}
}

which seems much more encapsulated and intuitive to me.


Persistence

On paper, the iPhone environment has a big advantage here: it features the Core Data object-persistence layer above the SQLite database, whereas Android requires direct DB access.

For me, though, direct DB access was not awful, and given the constraints of my app, arguably simpler than jumping through all of Core Data's hoops - using the separate tool to declare a data model, having to rebuild your custom code every time you add a column, etc. All that without even getting thread safety.

However, I'm totally comfortable writing SQL, not all developers are, and my app had pretty straightforward DB requirements. Core Data is undeniably more elegant and ultimately better. Plus you get nifty little features like shake-to-undo, semi-automatic migration data models in installed apps, etc. And it's not like the Android tools are particularly easy to work with. Check out the method in android.database.sqlite.SQLiteDatabase you use to perform a query:

public Cursor query (boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

As a (Google employee) friend of mine said, "Holy positional parameters, Batman!" Needless to say, one quickly wraps this monstrosity in other methods less prone to grievous error...


Part II of this post compares and contrasts system features, phone features, settings and resources, screen building, internet connectivity, and the app install/release process. (It does not compare and contrast graphics programming, as my apps are data-heavy not graphics-heavy.) Don't touch that dial.

Labels: , , , , , , , , , , , , ,


Sunday, January 31, 2010

 

Back in the Android saddle

So now that versions 2.0 of my iPhone app are up and running on the App Store, I've finally started writing an Android version. You may recall that this blog was supposed to be primarily about Android development from the get-go. We apologize for the eight-month sidetrack.

Anyway, It's been going quite well. Both the Android and iPhone SDK have their pros and cons. I like XCode better than Eclipse, and I generally (though not always) prefer Core Data to direct DB queries; on the other hand, string processing is far easier in Java, Android's multithreading is easier to work with, I vastly prefer single .java files vs twinned .h/.c files, and I love not having to worry about manual memory management.

The Android version is now mostly functionally complete. Things I have (sometimes re) learned which may be of interest:



More to come when I release v1.0, which should happen in the next ten days.

Labels: , , , , , , , ,


Wednesday, June 17, 2009

 

I am the alpha dog!

By which I mean, my Android app is now in alpha test: I just uploaded it onto my HTC Magic, and whaddaya know, it runs, it doesn't crash, it actually does stuff. Indeed, I just updated WikiTravel via its interface. (Slightly buggily, but hey, alpha test.)

Mind you, the Android app is a fairly trivial piece of programming next to the AppEngine middleware that does the actual parsing and updating of WikiTravel. Which is the way it should be. Resources are scarce on a smartphone, and both processing and battery power are relatively meagre; I expect the design pattern of choice is going to be "get as much of the work done as you can on your server farm, then communicate simple low-bandwidth stuff to the phone."

Ultimately, the phone isn't much more than a user interface. In fact, now that I think about it, my whole app follows the classic Model-View-Controller design: WikiTravel is the model, the smartphone is the view, and my AppEngine service is the controller. Huh. Plus ça change, plus c'est la même chose.

Anyway, a list of tips, tricks, and annoyances since last we met:

Labels: , , ,


Monday, May 25, 2009

 

I have been a good developer and deserve a cookie

...for I have spent the last couple of hours building Android JUnit test cases.

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:



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: , , , ,


Sunday, May 24, 2009

 

Background tasks and upfront dialogs

Android's process and threading model is fairly simple. Your application runs as a single process, but can spawn multiple threads, and is strongly encouraged to do so rather than block UI while waiting for, say, an upload or download to complete. (In fact, if you hang the main UI thread for to long, the OS will terminate your app.) You can do this one of two ways:



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



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: , , , ,


Friday, May 22, 2009

 

Well, that was easy.

So I created a new Google AppEngine project in Python: a very simple one, which just takes a particular HTTP POST request, stores its values to the datastore, and displays them on-screen. Then I added a "upload" function to my Android's DbHelper class, and connected the latter to the former.

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);

List paramList = 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: , , , , ,


Thursday, May 21, 2009

 

From Android to AppEngine

So I've finished the first (crude) iteration of my Android app, and am now moving on to creating a web application that will act as middleware.

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: , , , ,


Saturday, May 16, 2009

 

Things I learned about Android today

Since I'm at the foothill of the learning curve, I figure I should document as I go, as I'm learning more now than I will later. Spent most of the day coding, except for a break to go to yoga, and progress is good. Am about to go for beer, but before I do, a point-form list of what I learned and relearned today:



OK, beer beckons, more next week.

Labels: , ,


Friday, May 15, 2009

 

Pronoid Android

Hi! Welcome to 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:


  1. Create an Eclipse project with your working code. (In my case, the project is WTW, and the java package is "com.rezendi.wtw").
  2. Create an Eclipse project where test code will go. (In my case, WTWTest, java package "com.rezendi.wtw.test").
  3. 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.)
  4. 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>

  5. 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.
  6. 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.
  7. 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".
  8. Profit!

Labels: , , , , ,


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]