Ok, I have the following code in generateGeodatabaseWithParameters it fires fine and calculates the download speed. The problem I'm having is if a network disruption happens in the AGSResumableTaskJobStatus.FetchingResult status state, I no longer get status updates, and the call never timeouts. So to the user it just looks like their download has frozen forever. I have a cancel button and I can cancel it but I would rather detect the error and let the user knows that the generate process has failed during that step.
if status == AGSResumableTaskJobStatus.FetchingResult {
if let userinfo = userInfo {
totalBytesDownloaded = userinfo["AGSDownloadProgressTotalBytesDownloaded"] as? NSNumber
totalBytesExpected = userinfo["AGSDownloadProgressTotalBytesExpected"] as? NSNumber
if totalBytesDownloaded != nil && totalBytesExpected != nil {
elapsedTime = CACurrentMediaTime() - weakSelf.startTimeMapDl
elapsedSeconds = UInt(elapsedTime)
if elapsedSeconds > 0 && elapsedSeconds != previousSeconds {
var bytesPerSecond = UInt(0)
if previousSeconds == 0 {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.currentDownloadSpeedHeaderLabel.text = "Current D/L Speed:"
}
bytesPerSecond = totalBytesDownloaded!.unsignedLongValue / elapsedSeconds
}
else {
bytesPerSecond = (totalBytesDownloaded!.unsignedLongValue - previousBytesDownloaded.unsignedLongValue ) / (elapsedSeconds - previousSeconds )
}
//dl speed bounced between 0 and some other number if checked every second, so spreading it out over 3 seconds
//for a better UI experience. It still bounces around, but not as much.
let secondsDifference = elapsedSeconds - previousSeconds
if secondsDifference > 1 && previousBytesDownloaded.unsignedLongValue != totalBytesDownloaded!.unsignedLongValue {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.currentDownloadSpeedLabel.text = "\(NSByteCountFormatter.stringFromByteCount(Int64(bytesPerSecond), countStyle:NSByteCountFormatterCountStyle.File))"
}
previousBytesDownloaded = totalBytesDownloaded!
previousSeconds = elapsedSeconds
}
}
}
}
}
For other status states if there is an error I get a error message in the user info, but that does not populate or fire in the fetching result stage.
This at the top of the method call detects network errors for all states, but since FetchingResults never calls back, it can't run.
weakSelf.agsResumableTaskJob = weakSelf.gdbSyncTask?.generateGeodatabaseWithParameters(params,
downloadFolderPath: nil,
useExisting: false,
status: { status, userInfo in
if let userinfo = userInfo {
if let error = userinfo["statusRequestError"] {
print("Error: Could not generate geodatabase. Error details:\(error)")
errorOccured = true
weakSelf.agsResumableTaskJob?.cancel()
if error.localizedDescription == "The Internet connection appears to be offline." {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Network Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
else if error.localizedDescription == "A server with the specified hostname could not be found." {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Network Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
else {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
}
}
Using the latest sdk build (10.2.5)
Thanks for reporting this, Joshua
I can confirm that the status or completion blocks are not invoked when the download gets interrupted. We will look into this and evaluate what is the best behavior to implement going forward. Thinking about it some more, we don't want to prematurely fail the operation because even a slight hiccup in the network while downloading the result would fail the whole job and require the user to initiate the whole process again. As it stands today, eventhough the download appears to hang indefinitely, it does automatically restart when the network is restored. So on the bright side, it is resilient to transient network conditions. Ideally, we would have some internal timeout beyond which we would notify you and let you make the appropriate decision for your app.
In the meantime, if you want to provide a better UX where the user can put the download on ice when it appears to have hung, you could provide them an option saying "Try downloading later" which could be implemented using the pause/resume methods on AGSResumableTaskJob. It might be a little tricky to know when this option should be presented - but you could try and detect if the status block stops firing for X amount of time in the middle of fetching result, or you could always have a "Cancel" and "Try later" methods along with your download indicator incase the download runs into problems (even though canceling the whole job seems like a harsh thing to do, what you really want is for the user to try again later)
Thanks for the reply Divesh! I've noticed that most status resume once network picks up or it switches from wifi to cellular for example.
I am not seeing the behavior you describe during the fetch result stage. During the fetch result stage if I kill the network access and reenable I don't get status updates again.
Here is an example. As you can see in our enterprise app here, the download is going on. I then reach up and kill wifi (my only connection) to simulate a lost of network activity. At 3:30 I went up and reenabled the connection and as you can see the status never fired again because the progress just froze.
I already thought about detecting the no transfer for x seconds trick, and that will work if there is not another solution. Our users for this app are not technical GIS users, so we have to code in a lot of error handling and detection so they even know an issue is going on.
Hmmm, can you try this scenario with the DownloadTileCache sample ? I've seen that resume the download when network is restored.
That one did resume but paused a long wile after network was re-enabled but did eventually continue on.
The difference I saw when looking at the code is that it is a different task method:
[self.tileCacheTask exportTileCacheWithParameters:params downloadFolderPath:nil useExisting:YES status:^(AGSResumableTaskJobStatus status, NSDictionary *userInfo) {
vs generateGeodatbaseWithParameters
Let met try again with my code but time leave it up for a couple minutes.
Confirmed. I gave it several minutes and the status messages never came back.
Here is the whole method call and blocks:
let params = AGSGDBGenerateParameters(featureServiceInfo: weakSelf.gdbSyncTask.featureServiceInfo)
var totalBytesDownloaded : NSNumber?
var totalBytesExpected : NSNumber?
var errorOccured = false
weakSelf.agsResumableTaskJob = weakSelf.gdbSyncTask?.generateGeodatabaseWithParameters(params,
downloadFolderPath: nil,
useExisting: false,
status: { status, userInfo in
#if DEBUG
print("GDB Download Status: \(AGSResumableTaskJobStatusAsString(status))")
#endif
if let userinfo = userInfo {
if let error = userinfo["statusRequestError"] {
print("Error: Could not generate geodatabase. Error details:\(error)")
errorOccured = true
weakSelf.agsResumableTaskJob?.cancel()
if error.localizedDescription == "The Internet connection appears to be offline." {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Network Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the WSDOT Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
else if error.localizedDescription == "A server with the specified hostname could not be found." {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Network Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the WSDOT Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
else {
dispatch_async(dispatch_get_main_queue()) {
let alert = UIAlertController(title: "Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the WSDOT Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
}
}
}
if status == AGSResumableTaskJobStatus.Cancelled {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.stopTimer()
weakSelf.downloadButton.enabled = true
weakSelf.downloadButton.hidden = false
weakSelf.cancelDownloadButton.enabled = false
weakSelf.cancelDownloadButton.hidden = true
weakSelf.progressBar.progress = 0
weakSelf.timeElapsedLabel.text = "00:00:00";
weakSelf.progressFileSizeLabel.text = "0 MB of 0 MB"
weakSelf.progressPercentLabel.text = "0 %"
weakSelf.currentDownloadSpeedLabel.text = "0 MB"
weakSelf.currentDownloadSpeedHeaderLabel.text = "Current D/L Speed:"
}
}
else if status == AGSResumableTaskJobStatus.FetchingResult && errorOccured == false{
if let userinfo = userInfo {
totalBytesDownloaded = userinfo["AGSDownloadProgressTotalBytesDownloaded"] as? NSNumber
totalBytesExpected = userinfo["AGSDownloadProgressTotalBytesExpected"] as? NSNumber
if(weakSelf.startTimeMapDl == 0)
{
weakSelf.startTimeMapDl = NSDate().timeIntervalSinceReferenceDate
dispatch_async(dispatch_get_main_queue()) {
weakSelf.currentDownloadSpeedHeaderLabel.text = "Current D/L Speed:"
}
}
if totalBytesDownloaded != nil && totalBytesExpected != nil {
let percentage = totalBytesDownloaded!.doubleValue/totalBytesExpected!.doubleValue
dispatch_async(dispatch_get_main_queue()) {
weakSelf.progressBar.progress = Float((percentage/2) + 0.5)
weakSelf.progressFileSizeLabel.text = "\(NSByteCountFormatter.stringFromByteCount(totalBytesDownloaded!.longLongValue, countStyle:NSByteCountFormatterCountStyle.File)) of \(NSByteCountFormatter.stringFromByteCount(totalBytesExpected!.longLongValue, countStyle:NSByteCountFormatterCountStyle.File))"
}
let speed = totalBytesDownloaded!.doubleValue / Double((NSDate().timeIntervalSinceReferenceDate - weakSelf.startTimeMapDl))
if(speed != 0) {
weakSelf.totalSpeedInBytes += speed
weakSelf.numberOfSpeedReadings++
}
dispatch_async(dispatch_get_main_queue()) {
weakSelf.currentDownloadSpeedLabel.text = "\(NSByteCountFormatter.stringFromByteCount(Int64(speed), countStyle:NSByteCountFormatterCountStyle.File))"
}
}
}
}
else if errorOccured == false {
if status == AGSResumableTaskJobStatus.PreProcessingJob || status == AGSResumableTaskJobStatus.WaitingForDefaultParameters {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.progressFileSizeLabel.text = "Pre-Processing.."
}
}
if status == AGSResumableTaskJobStatus.StartingJob {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.progressFileSizeLabel.text = "Starting.."
}
}
if status == AGSResumableTaskJobStatus.Done {
dispatch_async(dispatch_get_main_queue()) {
if weakSelf.numberOfSpeedReadings != 0 {
let averageSpeedInBytes = weakSelf.totalSpeedInBytes / weakSelf.numberOfSpeedReadings
if averageSpeedInBytes != 0 {
weakSelf.currentDownloadSpeedHeaderLabel.text = "Average D/L Speed:"
weakSelf.currentDownloadSpeedLabel.text = "\(NSByteCountFormatter.stringFromByteCount(Int64(averageSpeedInBytes), countStyle:NSByteCountFormatterCountStyle.File))"
}
else {
weakSelf.currentDownloadSpeedHeaderLabel.text = "Average D/L Speed:"
weakSelf.currentDownloadSpeedLabel.text = "N/A"
}
}
else {
weakSelf.currentDownloadSpeedHeaderLabel.text = "Average D/L Speed:"
weakSelf.currentDownloadSpeedLabel.text = "N/A"
}
}
}
if status == AGSResumableTaskJobStatus.Polling {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.progressFileSizeLabel.text = "Acquiring map metadata. This can take several minutes."
}
}
dispatch_async(dispatch_get_main_queue()) {
if weakSelf.progressBar.progress < 0.5 {
weakSelf.progressBar.progress += 0.01
}
}
}
dispatch_async(dispatch_get_main_queue()) {
weakSelf.progressPercentLabel.text = "\(Int(weakSelf.progressBar.progress * 100)) %"
}
},
completion: { geodatabase, error in
if let error = error {
print("Error: Could not download the geodatabase from feature service. Error details: \(error)")
dispatch_async(dispatch_get_main_queue()) {
weakSelf.stopTimer()
weakSelf.downloadButton.enabled = true
weakSelf.downloadButton.hidden = false
weakSelf.cancelDownloadButton.enabled = false
weakSelf.cancelDownloadButton.hidden = true
weakSelf.progressBar.progress = 0
weakSelf.timeElapsedLabel.text = "00:00:00";
weakSelf.progressFileSizeLabel.text = "0 MB of 0 MB"
weakSelf.progressPercentLabel.text = "0 %"
weakSelf.currentDownloadSpeedLabel.text = "0 MB"
weakSelf.currentDownloadSpeedHeaderLabel.text = "Current D/L Speed:"
let alert = UIAlertController(title: "Error",
message: "Could not download the map inventory.\r\n\r\nPlease verify you are connected to the WSDOT Network and try again.",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
weakSelf.presentViewController(alert, animated: true, completion: nil)
}
return
}
else {
#if DEBUG
print("GDB download completed successfully, path is \(geodatabase.path)")
#endif
let featuresPath = Utility.getDocumentsDirectoryPath() + "/HATS-features.geodatabase"
var currentGeodatabaseDeletedOrDoesentExist = false
var currentGeodatabaseShmDeletedOrDoesentExist = false
var currentGeodatabaseWalDeletedOrDoesentExist = false
if( NSFileManager.defaultManager().isReadableFileAtPath(featuresPath)) {
do {
try NSFileManager.defaultManager().removeItemAtPath(featuresPath)
currentGeodatabaseDeletedOrDoesentExist = true
}
catch {
print("Could not delete the current geodatabase file.")
}
}
else
{
currentGeodatabaseDeletedOrDoesentExist = true
}
if( NSFileManager.defaultManager().isReadableFileAtPath(featuresPath + "-shm")) {
do {
try NSFileManager.defaultManager().removeItemAtPath(featuresPath + "-shm")
currentGeodatabaseShmDeletedOrDoesentExist = true
}
catch {
print("Could not delete the current geodatabase shm file.")
}
}
else
{
currentGeodatabaseShmDeletedOrDoesentExist = true
}
if( NSFileManager.defaultManager().isReadableFileAtPath(featuresPath + "-wal")) {
do {
try NSFileManager.defaultManager().removeItemAtPath(featuresPath + "-wal")
currentGeodatabaseWalDeletedOrDoesentExist = true
}
catch {
print("Could not delete the current geodatabase wal file.")
}
}
else
{
currentGeodatabaseWalDeletedOrDoesentExist = true
}
if currentGeodatabaseDeletedOrDoesentExist {
if( NSFileManager.defaultManager().isReadableFileAtPath(geodatabase.path)) {
do {
try NSFileManager.defaultManager().moveItemAtPath(geodatabase.path, toPath: featuresPath)
}
catch {
print("Could not move the downloaded geodatabase to its final path")
}
}
}
if currentGeodatabaseShmDeletedOrDoesentExist {
if( NSFileManager.defaultManager().isReadableFileAtPath(geodatabase.path + "-shm")) {
do {
try NSFileManager.defaultManager().moveItemAtPath(geodatabase.path + "-shm", toPath: featuresPath + "-shm")
}
catch {
print("Could not move the downloaded geodatabase shm to its final path")
}
}
}
if currentGeodatabaseWalDeletedOrDoesentExist {
if( NSFileManager.defaultManager().isReadableFileAtPath(geodatabase.path + "-wal")) {
do {
try NSFileManager.defaultManager().moveItemAtPath(geodatabase.path + "-wal", toPath: featuresPath + "-wal")
}
catch {
print("Could not move the downloaded geodatabase wal to its final path")
}
}
}
Utility.setPathToNotBackupByiCloud(featuresPath)
Utility.setPathToNotBackupByiCloud(featuresPath + "-shm")
Utility.setPathToNotBackupByiCloud(featuresPath + "-wal")
dispatch_async(dispatch_get_main_queue()) {
weakSelf.cancelDownloadButton.enabled = false
weakSelf.cancelDownloadButton.hidden = true
}
weakSelf.registerHatsMapFeaturesGdb()
}
}
)