A Simple Download Manager
Introduction
This report is
aimed at discussing a design for an iOS application, which downloads files from
the internet. Based on this design, the application is able to recover itself from
failures, which disrupt the download process and make it stop. The discussion
in this report covers the areas related to how the download states are saved in
case of a failure, and how the application uses the saved information to resume
previously in progress downloads. The example application uses ASIHTTPRequest library to perform network activities (in this case, simply downloading files), and uses ASINetworkQueue to manage multiple downloads. (A link to the source of the project discussed in this report is provided at the end of the report)
Application general description
The example download manager application uses two buttons to initiate downloads for a 5MB and a 10MB files. To enable an ASIHTTPRequest object to be able to resume not finished downloads, temporary download path can be set for partial downloads. Figure 1, shows the application in 3 different states.
Note that in figure 1, when downloads are in progress, network activity indicator is animating on the status bar. Figure 2 shows the partially downloaded files on the hard drive. Note that the
To provide the ability to recover in the download manager application, information of download requests is kept in an array of dictionaries, called dlTracker (NSMutableArray* dlTracker). Every request has a unique identifier so that it would be recognizable in the entire application. For this purpose, the tag property of ASIHTTPRequest objects is used. Download buttons code snippets are provided below. (A link to the source of the project is provided at the end of the report)
Code snippet 1: IBAction methods for initiating downloads
The application going to the background, network getting disconnected, and force stop by user; will be discussed as failure scenarios in the following. It should be mentioned that ASIHTTPRequest does not stop its file transferring process in the background. However, in this design, downloads are forced to stop in background, to better show the functionality of the application. A better design would be downloads remain in progress in background and be saved and stopped when the application is about to terminate.
Figure 1: The download manager application in 3 states: No downloads in queue, 1 download in progress and 2 download in progress |
Note that in figure 1, when downloads are in progress, network activity indicator is animating on the status bar. Figure 2 shows the partially downloaded files on the hard drive. Note that the
.downloadextension indicates that downloads are not completed.
To provide the ability to recover in the download manager application, information of download requests is kept in an array of dictionaries, called dlTracker (NSMutableArray* dlTracker). Every request has a unique identifier so that it would be recognizable in the entire application. For this purpose, the tag property of ASIHTTPRequest objects is used. Download buttons code snippets are provided below. (A link to the source of the project is provided at the end of the report)
- (IBAction)startDownloadFiveMBBtnPressed:(id)sender
{
if (file5MBDownloadInProgress)
return;
NSURL *url = [NSURL URLWithString:@"http://download.thinkbroadband.com/5MB.zip"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
NSString *downloadPath = [path stringByAppendingString:@"/5MB.zip"];
[request setDownloadDestinationPath:downloadPath];
[request setTemporaryFileDownloadPath:[path stringByAppendingString:@"/5MB.zip.download"]];
[request setAllowResumeForFileDownloads:YES];
[request setDelegate:self];
[request setDownloadProgressDelegate:progressViewFive];
[request setShowAccurateProgress:YES];
request.tag = 5;
//[request startAsynchronous];
[self takeRequestInfo:request];
[queue addOperation:request];
[queue go];
}
- (IBAction)startDownloadTenMBBtnPressed:(id)sender
{
if (file10MBDownloadInProgress)
return;
NSURL *url = [NSURL URLWithString:@"http://download.thinkbroadband.com/10MB.zip"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
NSString *downloadPath = [path stringByAppendingString:@"/10MB.zip"];
[request setDownloadDestinationPath:downloadPath];
[request setTemporaryFileDownloadPath:[path stringByAppendingString:@"/10MB.zip.download"]];
[request setAllowResumeForFileDownloads:YES];
[request setDelegate:self];
[request setDownloadProgressDelegate:progressViewTen];
[request setShowAccurateProgress:YES];
request.tag = 10;
//[request startAsynchronous];
[self takeRequestInfo:request];
[queue addOperation:request];
[queue go];
}
The application going to the background, network getting disconnected, and force stop by user; will be discussed as failure scenarios in the following. It should be mentioned that ASIHTTPRequest does not stop its file transferring process in the background. However, in this design, downloads are forced to stop in background, to better show the functionality of the application. A better design would be downloads remain in progress in background and be saved and stopped when the application is about to terminate.
First failure scenario: Application going to the background
When the application is about to enter background, downloads are stopped and the data being kept in dlTracker array is written to a file called “dlTracker.if”. Information kept for each download request includes, URL, download path, download temporary path and request unique identifier. Figures 3, shows that the dlTracker.if file comes into existence when the application enters background, and figure 4, shows the dlTracker file format.
It should be mentioned that dlTracker.if file is only created when there are download requests in download queue. Therefore, when re-entering foreground, the application uses this condition, which is the existence of dlTracker.if file, to determine if there were any download requests in progress when the application entered background. So, on changing state from background to foreground, if dlTracker.if file exists, user is asked for confirmation on resuming downloads. If user chooses to continue downloads, the requests are recreated based on their information in dlTracker.if file, and are resumed from the point indicated by partially downloaded files. As downloads begin to continue, dlTracker.if file is also removed.
If any of downloads finishes, its data will be removed from dlTracker array. Therefore, in case of any interruption, the finished process information will not be written to file. This is how not attempting to resume completed processes by the application is handled.
Code snippets for parts discussed in this section are provided below. (A link to the source of the project is provided at the end of the report)
- (void)takeRequestInfo:(ASIHTTPRequest*)request
{
NSString* requestTag = [[NSString alloc] initWithFormat:@"%d",request.tag];
NSString* url = [[NSString alloc] initWithString:[[request url] description]] ;
NSArray* objects = [[NSArray alloc] initWithObjects:
requestTag,url,
[request downloadDestinationPath],
[request temporaryFileDownloadPath],nil];
NSArray* keys = [[NSArray alloc] initWithObjects:
@"tag",@"url",
@"downloadPath",
@"tmpDownloadPath",nil];
[requestTag release];
[url release];
NSDictionary* requestInfo = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
[objects release];
[keys release];
[dlTracker addObject:requestInfo];
[requestInfo
release];
}
- (void)willResignActive:(NSNotification*)notification
{
[self performSelector:@selector(saveRequestInfo)];
}
- (void)saveRequestInfo
{
if ([dlTracker count]>0)
{
if (queue != nil)
[queue cancelAllOperations];
[dlTracker writeToFile:[path stringByAppendingString:@"/dlTracker.if"] atomically:YES];
[dlTracker removeAllObjects];
}
}
- (void)willEnterForeground:(NSNotification*)notification
{
[self performSelector:@selector(initializeNetworkOperation)];
}
- (void)initializeNetworkOperation
{
if (reachability == nil)
reachability = [[Reachability reachabilityForInternetConnection] retain];
[reachability stopNotifier];
[reachability startNotifier];
if ([reachability currentReachabilityStatus] == NotReachable)
{
[self performSelector:@selector(updateNetworkStatus:)
withObject:@"No Internet availability"];
withObject:@"No Internet availability"];
return;
}
[self performSelector:@selector(updateNetworkStatus:)
withObject:@"Internet available"];
withObject:@"Internet available"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reachabilityChanged:)
name:kReachabilityChangedNotification object:nil];
selector:@selector(reachabilityChanged:)
name:kReachabilityChangedNotification object:nil];
if (queue == nil)
{
queue = [[ASINetworkQueue alloc] init];
[queue setDelegate:self];
[queue setDownloadProgressDelegate:progressViewQ];
[queue setShowAccurateProgress:YES];
[queue setQueueDidFinishSelector:@selector(queueComplete:)];
[queue setRequestDidStartSelector:@selector(queueInProgress:)];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willEnterForeground:)
name:@"UIApplicationWillEnterForegroundNotification"
object:nil];
name:@"UIApplicationWillEnterForegroundNotification"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(willResignActive:)
name:@"UIApplicationWillResignActiveNotification"
object:nil];
object:nil];
if (dlTracker == nil)
dlTracker = [[NSMutableArray alloc] init];
if ([self performSelector:@selector(checkForIncompleteRequests)])
{
UIAlertView* alertView = [[UIAlertView alloc]
initWithTitle:@"Incomplete Downloads"
message:@"Continue incomplete downloads?"
delegate:self
cancelButtonTitle:@"No"
otherButtonTitles:@"Yes",nil];
initWithTitle:@"Incomplete Downloads"
message:@"Continue incomplete downloads?"
delegate:self
cancelButtonTitle:@"No"
otherButtonTitles:@"Yes",nil];
[alertView show];
[alertView release];
}
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1)
[self performSelector:@selector(recoverRequests)];
}
- (void)recoverRequests
{
NSMutableArray* mArray = [[NSMutableArray alloc] initWithContentsOfFile:[path stringByAppendingString:@"/dlTracker.if"]];
NSFileManager* fileManager = [NSFileManager defaultManager];
[fileManager
removeItemAtPath:[path stringByAppendingString:@"/dlTracker.if"] error:nil];
for (NSDictionary*
requestInfo in mArray)
{
NSURL *url = [[NSURL alloc] initWithString:[requestInfo objectForKey:@"url"]];
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:url];
[url release];
[request
setDownloadDestinationPath:[requestInfo objectForKey:@"downloadPath"]];
[request
setTemporaryFileDownloadPath:[requestInfo objectForKey:@"tmpDownloadPath"]];
NSNumber* requestTag = [requestInfo objectForKey:@"tag"];
request.tag = [requestTag intValue];
[request
setDelegate:self];
[request
setAllowResumeForFileDownloads:YES];
if (request.tag==5)
[request setDownloadProgressDelegate:progressViewFive];
else if
(request.tag == 10)
[request setDownloadProgressDelegate:progressViewTen];
[request
setShowAccurateProgress:YES];
[self takeRequestInfo:request];
[queue addOperation:request];
[request
release];
}
[mArray release];
if ( [queue operationCount]>0 )
[queue go];
}
Second failure scenario: Internet is not available
When the application is initialized, an object of type Reachability is created, which is constantly observing Internet connection. By every change in network status, Reachability object notifies the application of the change. This is where network failures are actually handled. As soon as the application is notified by a change in network status, and if it determines that the internet is not available anymore, dlTracker.if file is generated; containing the data related to active requests. When internet connection is back, saved requests are recovered through the same process as discussed before. Figure 8, illustrates the screenshots when internet is disconnected and connected again. Note that internet availability is shown on the application’s screen, and as mentioned before network activity indicator animates when requests are in progress. Code snippets for parts discussed in this section are provided below. (A link to the source of the project is provided at the end of the report)
- (void)reachabilityChanged:
(NSNotification* )note
{
Reachability* curReach = [note object];
NSParameterAssert([curReach
isKindOfClass: [Reachability class]]);
if ([curReach currentReachabilityStatus]==NotReachable)
[self performSelector:@selector(internetDisconnected)];
else
[self performSelector:@selector(internetConnected)];
}
- (void)internetDisconnected
{
NSLog(@"Damn it network's gone");
[self performSelector:@selector(updateNetworkStatus:) withObject:@"No Internet availability"];
[self saveRequestInfo];
}
- (void)internetConnected
{
NSLog(@"Phew, network's back");
[self performSelector:@selector(updateNetworkStatus:) withObject:@"Internet availabe"];
[self initializeNetworkOperation];
}
Third failure scenario: force stop by user
Two buttons, stop and resume, are provided for users, so that they can manage their download requests manually. The functionality of these buttons and what that happens in their IBAction methods is exactly the same as previous scenarios; except for the fact that unlike previous scenarios, when touching resume button, user is not asked for confirmation. This is in accordance with iOS Human Interface Guidelines 2011, which says it is not considered a good practice to take advantage of alert views to take users’ confirmation for tasks they have initiated themselves.
Conclusion
This download manager application is able to recover itself from almost any type of interruption. However, it may not be a good practice to keep track of requests similar to the way used here. Specially relying on existence of a file to determine if there are any downloads to resume, can be quite risky. Finally, as mentioned before, it would be better if downloads remained in progress in background and were saved and stopped when the application was about to terminate.
References
- Ben Copsey 2011, ASIHTTPRequest documentation, All-Seeing Interactive, viewed 8 November 2011, <http://allseeing-i.com/ASIHTTPRequest>.
- iOS Human Interface Guidelines 2011, iOS UI Element Usage Guidelines, Apple Inc., viewed 8 November 2011, <http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/MobileHIG/UIElementGuidelines/UIElementGuidelines.html#//apple_ref/doc/uid/TP40006556-CH13-SW1>.
- 5MB and 10MB Files are sourced from : <http://www.thinkbroadband.com/download.html>.
hi, I am iphone developer and visiting your blog today but in the link your source code is not located . kindly give me source code .My email address is attique2010@gmail.com
ReplyDeleteThis comment has been removed by the author.
DeleteKindly give me the source code of Automating Recovery after Failure I have facing the problems regarding network falure in my iphone application .
ReplyDeleteThanks for source code
ReplyDeleteHello! great article!! Im trying to do something similar as proof of concept. Please can you provide your source code as the link is broken. My email is james.13.n@gmail.com Thanks. Keep the good job
ReplyDeleteHI, Very helpful blog. Iam trying something similar can't find your source code. Can you please email me the code. Me email sacha2664@gmail.com. Thanks
ReplyDeleteHello, I love your blog. Could you please email me the code. My email is mlight.g@gmail.com. Thanks
ReplyDelete