Sunday, September 19, 2010

 

Of Zips and the Internet

So of late I've been working on adding the ability to download a complete snapshot of Wikitravel to my iTravel app. And I've finally got it working, for both Android and iPhone - but there was many a glitch along the way.

First off: downloading. Now that part's actually pretty easy. You know what's tricky? Working out whether you can download. Both Android and iPhone bury their "hey, am I connected to the Internet, and if so, how?" API facilities deep in their documentation.

To be fair, Android isn't so bad:

public static boolean CheckInternetConnection(Context context) {
if (context==null)
return false;
ConnectivityManager connec = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
return ( connec.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).isConnectedOrConnecting() ||
connec.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnectedOrConnecting() );
}


...straightforward enough, just not as well-documented as it could be.

The iPhone, though. Well. Sheesh. The way you actually do it is through seriously byzantine and messy C code. Because most people don't want to have to deal with that, Apple provides a reference implementation - but do they tell you about this anywhere in their documentation? No, they do not. Thank heavens for Stack Overflow, which pointed me to Reachability.

But wait, there's less: the old version has a very simple interface:

+(BOOL) serverReachable {
Reachability *reachability = [Reachability reachabilityWithHostName:@"http://itravelapp.appspot.com/"];
return [reachability currentReachabilityStatus]!=NotReachable;
}


but the new version requires you to set up a whole asynchronous notification framework. OK, so it probably works better, and it's not too onerous:


-(void) initiateReachability {
// check for internet connection
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkNetworkStatus:) name:kReachabilityChangedNotification object:nil];

internetReachable = [[Reachability reachabilityForInternetConnection] retain];
[internetReachable startNotifier];

// check if a pathway to my server exists
serverReachable = [[Reachability reachabilityWithHostName: @"itravelapp.appspot.com"] retain];
[serverReachable startNotifier];
}

+(void) initiateReachability {
[[self getInstance] initiateReachability];
}

- (void) checkNetworkStatus:(NSNotification *)notice
{
// called after network status changes

NetworkStatus internetStatus = [internetReachable currentReachabilityStatus];
internetActive = internetStatus != NotReachable;

NetworkStatus serverStatus = [serverReachable currentReachabilityStatus];
serverActive = serverStatus != NotReachable;
}

-(BOOL) serverReachable {
return serverActive;
}

+(BOOL) serverReachable {
return [[self getInstance] serverReachable];
}


But still, compare and contrast to the Android version above. Oh, Apple. Whatever were you thinking?

OK. So you've got the Internet. You've downloaded your zip files. (Yes, files, plural; one of my early problems is that both Androids and iPhones choke and die on 100MB zip files, due to their memory constraints. But five 20MB zip files, handled consecutively, no problem.)

Now how do you open them?

In Android, it's easy enough: java.util.zip.ZipFile and java.util.zip.ZipEntry. (In fact, it's easy - and fast - enough that I don't even unpack those files; I just access data from within them on the fly.) On the iPhone, though, although the libz.1.2.3.dylib zlib framework is provided, actually working with it requires some serious low-level C chops, and mine are rusty beyond belief.

But that's OK! Because others' aren't, and we live in an open-source world. I just went out and downloaded Karl Mostowski's truly excellent ZipKit framework, and used it. I had some trouble using it as a static library... so I gave up and just included his source in my project. And it worked like a charm. A diamond-studded, streamlined, memory-miserly charm. Thanks, Karl!

Here's an example of it in action:

-(void) doUnpacking {
[Util showActivity];
[self startProgressView];
self.title=@"Unpacking ...";
NSThread* unpackThread = [[NSThread alloc] initWithTarget:self selector:@selector(launchUnpacking) object:nil];
[unpackThread start];
[unpackThread release];
}

-(void) launchUnpacking {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

int zipFiles = [[Settings getBulkZipFiles] intValue];
if (zipFiles > 0)
[Settings setBulkUnpackedFiles:0];

for (int i=0; i < zipFiles; i++) {
NSString *titleString = [NSString stringWithFormat:@"Unpacking %d of %d", i+1, zipFiles];
[self performSelectorOnMainThread:@selector(setTitle:) withObject:titleString waitUntilDone:YES];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
NSString *fileName = [NSString stringWithFormat:@"/iTravelDump%d.zip",i];
NSString *dumpPath = [[Util getBulkDownloadPath] stringByAppendingPathComponent:fileName];
ZKFileArchive *archive = [ZKFileArchive archiveWithArchivePath:dumpPath];
archive.delegate = self;
totalFiles = [archive.centralDirectory count];
currentFile=0;
[archive inflateToDirectory:[[Util getBulkDownloadPath] stringByAppendingPathComponent:@"temp"] usingResourceFork:NO];
archive=nil;
[pool2 release];
[Settings setBulkUnpackedFiles:[Settings getBulkUnpackedFiles]+totalFiles];
}

//done!
[self performSelectorOnMainThread:@selector(unpackingFinished) withObject:nil waitUntilDone:NO];
[pool release];
}

//ZKArchive delegate
- (void) onZKArchive:(ZKArchive *) archive willUnzipPath:(NSString *) path {
if (progressView) {
float currentLength = (float)++currentFile;
float totalSize = (float)totalFiles;
float fraction = (float)currentLength/totalSize;
[progressView setProgress:fraction];
}
}

-(void)unpackingFinished {
[Util stopShowingActivity];
[self stopProgressView];
[Settings setLastBulkUnpack:[NSDate date]];
[Settings save];
self.title=@"Unpacking Complete";
[self loadHTML];
}

Labels: , , , , ,


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

Subscribe to Posts [Atom]