diff --git a/matrixConsole/MediaManager.h b/matrixConsole/MediaManager.h index df5974d46..5e9e077b1 100644 --- a/matrixConsole/MediaManager.h +++ b/matrixConsole/MediaManager.h @@ -24,16 +24,21 @@ extern NSString *const kMediaManagerProgressStringKey; extern NSString *const kMediaManagerProgressRemaingTimeKey; extern NSString *const kMediaManagerProgressDownloadRateKey; -// provide the download progress +// provide the download/upload progress // object: URL // userInfo: kMediaManagerProgressRateKey : progress value nested in a NSNumber (range 0->1) -// : kMediaManagerProgressStringKey : progress string XXX KB / XXX MB" -// : kMediaManagerProgressRemaingTimeKey : remaining time string "XX s left" -// : kMediaManagerProgressDownloadRateKey : string like XX MB/s +// : kMediaManagerProgressStringKey : progress string XXX KB / XXX MB" (optional) +// : kMediaManagerProgressRemaingTimeKey : remaining time string "XX s left" (optional) +// : kMediaManagerProgressDownloadRateKey : string like XX MB/s (optional) extern NSString *const kMediaDownloadProgressNotification; extern NSString *const kMediaUploadProgressNotification; +// notify when a media download is finished +// object: URL +extern NSString *const kMediaDownloadDidFinishNotification; +extern NSString *const kMediaDownloadDidFailNotification; + // The callback blocks typedef void (^blockMediaManager_onImageReady)(UIImage *image); typedef void (^blockMediaManager_onMediaReady)(NSString *cacheFilePath); @@ -45,6 +50,10 @@ typedef void (^blockMediaManager_onError)(NSError *error); + (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size; +// get a picture from the local cache +// do not start any remote requests ++ (UIImage*)loadCachePicture:(NSString*)pictureURL; + // Load a picture from the local cache or download it if it is not available yet. // In this second case a mediaLoader reference is returned in order to let the user cancel this action. + (id)loadPicture:(NSString *)pictureURL @@ -56,6 +65,14 @@ typedef void (^blockMediaManager_onError)(NSError *error); mimeType:(NSString *)mimeType success:(blockMediaManager_onMediaReady)success failure:(blockMediaManager_onError)failure; + +// try to find out a media loder from a media URL ++ (id)mediaLoaderForURL:(NSString*)url; + +// same dictionary as the kMediaDownloadProgressNotification one ++ (NSDictionary*)downloadStatsDict:(id)mediaLoader; + +// cancel a media loader + (void)cancel:(id)mediaLoader; + (NSString *)cacheMediaData:(NSData *)mediaData forURL:(NSString *)mediaURL mimeType:(NSString *)mimeType; diff --git a/matrixConsole/MediaManager.m b/matrixConsole/MediaManager.m index 9cb3b1a44..6c324d85e 100644 --- a/matrixConsole/MediaManager.m +++ b/matrixConsole/MediaManager.m @@ -22,6 +22,9 @@ NSString *const kMediaManagerPrefixForDummyURL = @"dummyUrl-"; NSString *const kMediaDownloadProgressNotification = @"kMediaDownloadProgressNotification"; NSString *const kMediaUploadProgressNotification = @"kMediaUploadProgressNotification"; +NSString *const kMediaDownloadDidFinishNotification = @"kMediaDownloadDidFinishNotification"; +NSString *const kMediaDownloadDidFailNotification = @"kMediaDownloadDidFailNotification"; + NSString *const kMediaManagerProgressRateKey = @"kMediaManagerProgressRateKey"; NSString *const kMediaManagerProgressStringKey = @"kMediaManagerProgressStringKey"; NSString *const kMediaManagerProgressRemaingTimeKey = @"kMediaManagerProgressRemaingTimeKey"; @@ -49,15 +52,47 @@ static MediaManager *sharedMediaManager = nil; CFAbsoluteTime downloadStartTime; CFAbsoluteTime lastProgressEventTimeStamp; NSTimer* progressCheckTimer; - - // keep to a copy of the upload dict - NSMutableDictionary* uploadDict; } + ++ (MediaLoader*)mediaLoaderForURL:(NSString*)url; + +@property (strong, nonatomic) NSMutableDictionary* downloadStatsDict; @end #pragma mark - MediaLoader @implementation MediaLoader +@synthesize downloadStatsDict; + +// find a MediaLoader +static NSMutableDictionary* pendingMediaLoadersByURL = nil; + ++ (MediaLoader*)mediaLoaderForURL:(NSString*)url { + MediaLoader* res = nil; + + if (pendingMediaLoadersByURL && url) { + res = [pendingMediaLoadersByURL valueForKey:url]; + } + + return res; +} + ++ (void) setMediaLoader:(MediaLoader*)mediaLoader forURL:(NSString*)url { + if (!pendingMediaLoadersByURL) { + pendingMediaLoadersByURL = [[NSMutableDictionary alloc] init]; + } + + // sanity check + if (mediaLoader && url) { + [pendingMediaLoadersByURL setValue:mediaLoader forKey:url]; + } +} + ++ (void) removeMediaLoaderWithUrl:(NSString*)url { + if (url) { + [pendingMediaLoadersByURL removeObjectForKey:url]; + } +} - (NSString*)validateContentURL:(NSString*)contentURL { // Detect matrix content url @@ -70,27 +105,37 @@ static MediaManager *sharedMediaManager = nil; return contentURL; } + - (void)downloadPicture:(NSString*)pictureURL success:(blockMediaManager_onImageReady)success failure:(blockMediaManager_onError)failure { + + [MediaLoader setMediaLoader:self forURL:pictureURL]; + // Download picture content [self downloadMedia:pictureURL mimeType:@"image/jpeg" success:^(NSString *cacheFilePath) { + [MediaLoader removeMediaLoaderWithUrl:pictureURL]; + if (success) { NSData* imageContent = [NSData dataWithContentsOfFile:cacheFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil]; if (imageContent) { UIImage *image = [UIImage imageWithData:imageContent]; if (image) { success(image); + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFinishNotification object:pictureURL userInfo:nil]; } else { NSLog(@"ERROR: picture download failed: %@", pictureURL); if (failure){ + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:pictureURL userInfo:nil]; failure(nil); } } } } } failure:^(NSError *error) { + [MediaLoader removeMediaLoaderWithUrl:pictureURL]; failure(error); + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:pictureURL userInfo:nil]; }]; } @@ -107,6 +152,8 @@ static MediaManager *sharedMediaManager = nil; downloadStartTime = statsStartTime = CFAbsoluteTimeGetCurrent(); lastProgressEventTimeStamp = -1; + [MediaLoader setMediaLoader:self forURL:mediaURL]; + // Start downloading NSURL *url = [NSURL URLWithString:[self validateContentURL:aMediaURL]]; downloadData = [[NSMutableData alloc] init]; @@ -114,6 +161,7 @@ static MediaManager *sharedMediaManager = nil; } - (void)cancel { + [MediaLoader removeMediaLoaderWithUrl:mediaURL]; // Reset blocks onMediaReady = nil; onError = nil; @@ -138,9 +186,13 @@ static MediaManager *sharedMediaManager = nil; - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"ERROR: media download failed: %@, %@", error, mediaURL); + [MediaLoader removeMediaLoaderWithUrl:mediaURL]; + // send the latest known upload info [self progressCheckTimeout:nil]; - uploadDict = nil; + downloadStatsDict = nil; + + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil]; if (onError) { onError (error); @@ -184,15 +236,13 @@ static MediaManager *sharedMediaManager = nil; statsStartTime = currentTime; - // only one parameter by now + // build the user info dictionary NSMutableDictionary* dict = [[NSMutableDictionary alloc] init]; [dict setValue:[NSNumber numberWithFloat:rate] forKey:kMediaManagerProgressRateKey]; - NSString* progressString = [NSString stringWithFormat:@"%@ / %@", [NSByteCountFormatter stringFromByteCount:downloadData.length countStyle:NSByteCountFormatterCountStyleFile], [NSByteCountFormatter stringFromByteCount:expectedSize countStyle:NSByteCountFormatterCountStyleFile]]; [dict setValue:progressString forKey:kMediaManagerProgressStringKey]; - NSMutableString* remaingTimeStr = [[NSMutableString alloc] init]; if (dataRemainingTime < 1) { @@ -218,7 +268,7 @@ static MediaManager *sharedMediaManager = nil; NSString* downloadRateStr = [NSString stringWithFormat:@"%@/s", [NSByteCountFormatter stringFromByteCount:meanRate * 1024 countStyle:NSByteCountFormatterCountStyleFile]]; [dict setValue:downloadRateStr forKey:kMediaManagerProgressDownloadRateKey]; - uploadDict = dict; + downloadStatsDict = dict; // after 0.1s, resend the progress info // the upload can be stuck @@ -228,8 +278,7 @@ static MediaManager *sharedMediaManager = nil; // trigger the event only each 0.1s to avoid send to many events if ((lastProgressEventTimeStamp == -1) || ((currentTime - lastProgressEventTimeStamp) > 0.1)) { lastProgressEventTimeStamp = currentTime; - - [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:uploadDict]; + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:downloadStatsDict]; } } } @@ -237,18 +286,19 @@ static MediaManager *sharedMediaManager = nil; - (IBAction) progressCheckTimeout:(id)sender { // remove the bitrate -> can be invalid - [uploadDict removeObjectForKey:kMediaManagerProgressDownloadRateKey]; + [downloadStatsDict removeObjectForKey:kMediaManagerProgressDownloadRateKey]; - [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:uploadDict]; + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:downloadStatsDict]; [progressCheckTimer invalidate]; progressCheckTimer = nil; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { + [MediaLoader removeMediaLoaderWithUrl:mediaURL]; // send the latest known upload info [self progressCheckTimeout:nil]; - uploadDict = nil; + downloadStatsDict = nil; if (downloadData.length) { // Cache the downloaded data @@ -257,11 +307,14 @@ static MediaManager *sharedMediaManager = nil; if (onMediaReady) { onMediaReady(cacheFilePath); } + + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFinishNotification object:mediaURL userInfo:nil]; } else { NSLog(@"ERROR: media download failed: %@", mediaURL); if (onError){ onError(nil); } + [[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil]; } downloadData = nil; @@ -375,6 +428,15 @@ static MediaManager *sharedMediaManager = nil; return ret; } +// try to find out a media loder from a media URL ++ (id)mediaLoaderForURL:(NSString*)url { + return [MediaLoader mediaLoaderForURL:url]; +} + ++ (NSDictionary*)downloadStatsDict:(id)mediaLoader { + return ((MediaLoader*)mediaLoader).downloadStatsDict; +} + + (void)cancel:(id)mediaLoader { [((MediaLoader*)mediaLoader) cancel]; } diff --git a/matrixConsole/View/CustomImageView.m b/matrixConsole/View/CustomImageView.m index 731271396..43d14d4ac 100644 --- a/matrixConsole/View/CustomImageView.m +++ b/matrixConsole/View/CustomImageView.m @@ -79,13 +79,13 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil]; [self stopActivityIndicator]; - if (imageLoader) { - [MediaManager cancel:imageLoader]; - imageLoader = nil; - } + imageLoader = nil; + if (loadingView) { [loadingView removeFromSuperview]; loadingView = nil; @@ -289,6 +289,8 @@ [super removeFromSuperview]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil]; if (useFullScreen) { [UIApplication sharedApplication].statusBarHidden = NO; @@ -435,13 +437,13 @@ // it could be triggered after a screen rotation, new message ... return; } + + // reinit parameters + imageLoader = nil; + downloadingImageURL = nil; - // Cancel media loader in progress (if any) - if (imageLoader) { - [MediaManager cancel:imageLoader]; - imageLoader = nil; - downloadingImageURL = nil; - } + // remove any pending observers + [[NSNotificationCenter defaultCenter] removeObserver:self]; // preview image until the image is loaded self.image = previewImage; @@ -459,21 +461,102 @@ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadProgress:) name:kMediaDownloadProgressNotification object:nil]; - imageLoader = [MediaManager loadPicture:downloadingImageURL - success:^(UIImage *anImage) { - downloadingImageURL = nil; - [self stopActivityIndicator]; - self.image = anImage; - loadedImageURL = anImageURL; - - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; - } - failure:^(NSError *error) { - [self stopActivityIndicator]; - downloadingImageURL = nil; - NSLog(@"Failed to download image (%@): %@", anImageURL, error); - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; - }]; + id loader = [MediaManager mediaLoaderForURL:downloadingImageURL]; + + // there is no pending request to this URL + if (!loader) { + imageLoader = [MediaManager loadPicture:downloadingImageURL + success:^(UIImage *anImage) { + downloadingImageURL = nil; + [self stopActivityIndicator]; + self.image = anImage; + loadedImageURL = anImageURL; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; + } + failure:^(NSError *error) { + [self stopActivityIndicator]; + downloadingImageURL = nil; + NSLog(@"Failed to download image (%@): %@", anImageURL, error); + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; + }]; + } else { + // update the progress UI with the current info + [self updateProgressUI:[MediaManager downloadStatsDict:loader]]; + + // wait that the download is ended + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil]; + } + } +} + +- (void)onMediaDownloadEnd:(NSNotification *)notif { + // sanity check + if ([notif.object isKindOfClass:[NSString class]]) { + NSString* url = notif.object; + + if ([url isEqualToString:downloadingImageURL]) { + [self stopActivityIndicator]; + + // update the image + UIImage* image = [MediaManager loadCachePicture:downloadingImageURL]; + + if (image) { + self.image = image; + } + + // updates the statuses + loadedImageURL = downloadingImageURL; + downloadingImageURL = nil; + + // remove the observers + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadProgressNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaDownloadDidFailNotification object:nil]; + } + } +} + + +- (void)updateProgressUI:(NSDictionary*)downloadStatsDict { + NSNumber* progressNumber = [downloadStatsDict valueForKey:kMediaManagerProgressRateKey]; + + if (progressNumber) { + pieChartView.progress = progressNumber.floatValue; + waitingDownloadSpinner.hidden = YES; + } + + if (progressInfoLabel) { + NSString* downloadRate = [downloadStatsDict valueForKey:kMediaManagerProgressDownloadRateKey]; + NSString* remaingTime = [downloadStatsDict valueForKey:kMediaManagerProgressRemaingTimeKey]; + NSString* progressString = [downloadStatsDict valueForKey:kMediaManagerProgressStringKey]; + + NSMutableString* text = [[NSMutableString alloc] init]; + + [text appendString:progressString]; + + if (remaingTime) { + [text appendFormat:@" (%@)", remaingTime]; + } + + if (downloadRate) { + [text appendFormat:@"\n %@", downloadRate]; + } + + progressInfoLabel.text = text; + + // on multilines, sizeToFit uses the current width + // so reset it + progressInfoLabel.frame = CGRectZero; + + [progressInfoLabel sizeToFit]; + + // + CGRect progressInfoLabelFrame = progressInfoLabel.frame; + progressInfoLabelFrame.origin.x = self.center.x - (progressInfoLabelFrame.size.width / 2); + progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height; + progressInfoLabel.frame = progressInfoLabelFrame; } } @@ -483,44 +566,7 @@ NSString* url = notif.object; if ([url isEqualToString:downloadingImageURL]) { - NSNumber* progressNumber = [notif.userInfo valueForKey:kMediaManagerProgressRateKey]; - - if (progressNumber) { - pieChartView.progress = progressNumber.floatValue; - waitingDownloadSpinner.hidden = YES; - } - - if (progressInfoLabel) { - NSString* downloadRate = [notif.userInfo valueForKey:kMediaManagerProgressDownloadRateKey]; - NSString* remaingTime = [notif.userInfo valueForKey:kMediaManagerProgressRemaingTimeKey]; - NSString* progressString = [notif.userInfo valueForKey:kMediaManagerProgressStringKey]; - - NSMutableString* text = [[NSMutableString alloc] init]; - - [text appendString:progressString]; - - if (remaingTime) { - [text appendFormat:@" (%@)", remaingTime]; - } - - if (downloadRate) { - [text appendFormat:@"\n %@", downloadRate]; - } - - progressInfoLabel.text = text; - - // on multilines, sizeToFit uses the current width - // so reset it - progressInfoLabel.frame = CGRectZero; - - [progressInfoLabel sizeToFit]; - - // - CGRect progressInfoLabelFrame = progressInfoLabel.frame; - progressInfoLabelFrame.origin.x = self.center.x - (progressInfoLabelFrame.size.width / 2); - progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height; - progressInfoLabel.frame = progressInfoLabelFrame; - } + [self updateProgressUI:notif.userInfo]; } } }