#import "TGImageDownloadActor.h" #import "TGAppDelegate.h" #import "ActionStage.h" #import "SGraphObjectNode.h" #import "TGRemoteImageView.h" #import "TGImageUtils.h" #import "TGStringUtils.h" #import "TGTelegraph.h" #import "TGTelegraphProtocols.h" #import "TGFileDownloadActor.h" #import "TGGenericModernConversationCompanion.h" #import "TGDownloadManager.h" #import "TGDatabase.h" #import "TGInterfaceAssets.h" #import "TGImageManager.h" #import "TGImagePickerController.h" static NSMutableDictionary *urlRewrites() { static NSMutableDictionary *dict = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^ { dict = [[NSMutableDictionary alloc] init]; }); return dict; } static NSMutableDictionary *serverAssetData() { static NSMutableDictionary *dict = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^ { dict = [[NSMutableDictionary alloc] init]; }); return dict; } typedef void (^TGRemoteImageDownloadCompletionBlock)(NSData *data); @interface TGImageDownloadActor () { bool _updateMediaAccessTimeOnRelease; int32_t _messageId; int64_t _imageId; } @property (nonatomic, copy) TGRemoteImageDownloadCompletionBlock downloadCompletionBlock; @property (nonatomic) float progress; @property (nonatomic) bool requestedActors; @end @implementation TGImageDownloadActor @synthesize actionHandle = _actionHandle; @synthesize downloadCompletionBlock = _downloadCompletionBlock; @synthesize progress = _progress; @synthesize requestedActors = _requestedActors; + (NSString *)genericPath { return @"/img/@"; } + (void)addUrlRewrite:(NSString *)currentUrl newUrl:(NSString *)newUrl { [urlRewrites() setObject:newUrl forKey:currentUrl]; } + (NSString *)possiblyRewrittenUrl:(NSString *)url { NSString *newUrl = [urlRewrites() objectForKey:url]; if (newUrl != nil) return newUrl; return url; } + (NSDictionary *)serverMediaDataForAssetUrl:(NSString *)assetUrl { if (assetUrl.length == 0) return nil; id object = [serverAssetData() objectForKey:assetUrl]; if ([object isKindOfClass:[NSNull class]]) return nil; if ([object isKindOfClass:[NSDictionary class]]) return object; TGMediaAttachment *attachment = [TGDatabaseInstance() loadServerAssetData:assetUrl]; if (attachment == nil) { [serverAssetData() setObject:[NSNull null] forKey:assetUrl]; return nil; } NSDictionary *dict = nil; if (attachment.type == TGImageMediaAttachmentType) { TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment; imageAttachment.caption = nil; dict = [[NSDictionary alloc] initWithObjectsAndKeys:[[NSNumber alloc] initWithLongLong:imageAttachment.imageId], @"imageId", [[NSNumber alloc] initWithLongLong:imageAttachment.accessHash], @"accessHash", imageAttachment, @"imageAttachment", nil]; [serverAssetData() setObject:dict forKey:assetUrl]; return dict; } else if (attachment.type == TGVideoMediaAttachmentType) { TGVideoMediaAttachment *videoAttachment = (TGVideoMediaAttachment *)attachment; videoAttachment.caption = nil; dict = [[NSDictionary alloc] initWithObjectsAndKeys:videoAttachment, @"videoAttachment", nil]; [serverAssetData() setObject:dict forKey:assetUrl]; return dict; } return nil; } + (void)addServerMediaSataForAssetUrl:(NSString *)assetUrl attachment:(TGMediaAttachment *)attachment { if (assetUrl.length == 0 || attachment == nil) return; [TGDatabaseInstance() storeServerAssetData:assetUrl attachment:attachment]; if (attachment.type == TGImageMediaAttachmentType) { TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment; NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:[[NSNumber alloc] initWithLongLong:imageAttachment.imageId], @"imageId", [[NSNumber alloc] initWithLongLong:imageAttachment.accessHash], @"accessHash", imageAttachment, @"imageAttachment", nil]; [serverAssetData() setObject:dict forKey:assetUrl]; } else if (attachment.type == TGVideoMediaAttachmentType) { TGVideoMediaAttachment *videoAttachment = (TGVideoMediaAttachment *)attachment; NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:videoAttachment, @"videoAttachment", nil]; [serverAssetData() setObject:dict forKey:assetUrl]; } } + (NSOperationQueue *)operationQueue { static NSOperationQueue *queue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^ { queue = [[NSOperationQueue alloc] init]; [queue setMaxConcurrentOperationCount:(cpuCoreCount() > 1 ? 3 : 2)]; }); return queue; } - (id)initWithPath:(NSString *)path { self = [super initWithPath:path]; if (self != nil) { _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:false]; } return self; } - (void)prepare:(NSDictionary *)options { if (options != nil) { int contentHints = [[options objectForKey:@"contentHints"] intValue]; if (contentHints & TGRemoteImageContentHintLargeFile) { if ([[self.path substringFromIndex:6] hasPrefix:@"download:"]) self.requestQueueName = @"imageDownload"; } } } - (void)dealloc { [_actionHandle reset]; if (_requestedActors) [ActionStageInstance() removeWatcher:self]; } static inline double imageProcessingPriority() { return !TGIsRetina() ? 0.12 : (cpuCoreCount() > 1 ? 0.4 : 0.1); } - (void)execute:(NSDictionary *)options { static CFAbsoluteTime lastCompletionTime = 0; static bool delayFastCompletion = false; /*static dispatch_once_t onceToken; dispatch_once(&onceToken, ^ { delayFastCompletion = cpuCoreCount() < 2; });*/ const CFTimeInterval fastDelayThresold = 0.018; const CFTimeInterval minimalThreshold = 0.008; bool allowThumbnailCache = false; bool allowMemoryCache = true; int contentHints = 0; id userProperties = nil; bool forceMemoryCache = false; TGCache *cache = nil; if (options != nil) { NSNumber *cancelTimeout = [options objectForKey:@"cancelTimeout"]; if (cancelTimeout != nil) self.cancelTimeout = [cancelTimeout intValue]; cache = [options objectForKey:@"cache"]; contentHints = [[options objectForKey:@"contentHints"] intValue]; NSNumber *nAllowThumbnailCache = [options objectForKey:@"allowThumbnailCache"]; if (nAllowThumbnailCache != nil) allowThumbnailCache = [nAllowThumbnailCache boolValue]; NSNumber *nUseCache = [options objectForKey:@"useCache"]; if (nUseCache != nil) allowMemoryCache = [nUseCache boolValue]; userProperties = [options objectForKey:@"userProperties"]; forceMemoryCache = [options objectForKey:@"forceMemoryCache"]; } if (!allowMemoryCache) TGLog(@"Memory cache disabled for %@", self.path); if (cache == nil) cache = [TGRemoteImageView sharedCache]; NSString *path = self.path; NSString *actualPath = self.path; if ([actualPath hasPrefix:@"/img/(download:"]) actualPath = [actualPath stringByReplacingOccurrencesOfString:@"/img/(download:" withString:@"/img/("]; NSString *url = nil; TGImageProcessor processor = nil; NSString *processorName = nil; if ([actualPath hasPrefix:@"/img/({filter:"]) { NSRange range = [actualPath rangeOfString:@"}"]; if (range.location == NSNotFound) { [ActionStageInstance() nodeRetrieveFailed:self.path]; return; } processorName = [actualPath substringWithRange:NSMakeRange(14, range.location - 14)]; processor = [TGRemoteImageView imageProcessorForName:processorName]; url = [actualPath substringWithRange:NSMakeRange(range.location + 1, actualPath.length - range.location - 1 - 1)]; } else url = [actualPath substringWithRange:NSMakeRange(6, actualPath.length - 6 - 1)]; NSString *rewrittenUrl = [TGImageDownloadActor possiblyRewrittenUrl:url]; url = rewrittenUrl; NSString *storeUrl = url; if (processor != nil) storeUrl = [[NSString alloc] initWithFormat:@"{filter:%@}%@", processorName, url]; bool cacheFiltered = [processorName hasSuffix:@"+bake"]; if ([url hasPrefix:@"asset-original:"] || [url hasPrefix:@"asset-thumbnail:"]) { NSString *assetUrl = [url substringFromIndex:[url rangeOfString:@":"].location + 1]; [TGImagePickerController loadAssetWithUrl:[[NSURL alloc] initWithString:assetUrl] completion:^(ALAsset *asset) { UIImage *image = nil; if (asset != nil) { if ([url hasPrefix:@"asset-original:"]) { UIImage *rawImage = [[UIImage alloc] initWithCGImage:asset.defaultRepresentation.fullScreenImage]; if (processor != nil) image = processor(rawImage); else image = [rawImage preloadedImage]; if (image != nil && allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:image withData:nil url:storeUrl availability:TGCacheMemory]; } else { image = [[UIImage alloc] initWithCGImage:asset.aspectRatioThumbnail]; } } if (image != nil) [ActionStageInstance() nodeRetrieved:path node:[[SGraphObjectNode alloc] initWithObject:image]]; else [ActionStageInstance() nodeRetrieveFailed:path]; }]; return; } UIImage *imageManagerImage = [[TGImageManager instance] loadImageSyncWithUri:url canWait:true decode:processor == nil acceptPartialData:false asyncTaskId:NULL progress:nil partialCompletion:nil completion:nil]; if (imageManagerImage != nil) { UIImage *image = processor != nil ? processor(imageManagerImage) : imageManagerImage; if (image != nil && allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:image withData:nil url:storeUrl availability:TGCacheMemory]; if (image != nil) [ActionStageInstance() nodeRetrieved:path node:[[SGraphObjectNode alloc] initWithObject:image]]; else [ActionStageInstance() nodeRetrieveFailed:path]; } if ([url hasPrefix:@"dialogListPlaceholder:"]) { int64_t conversationId = [[url substringFromIndex:@"dialogListPlaceholder:".length] longLongValue]; UIImage *image = conversationId < 0 ? [[TGInterfaceAssets instance] groupAvatarPlaceholder:conversationId] : [[TGInterfaceAssets instance] avatarPlaceholder:(int)conversationId]; if (image != nil && allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:image withData:nil url:storeUrl availability:TGCacheMemory]; if (image != nil) [ActionStageInstance() nodeRetrieved:path node:[[SGraphObjectNode alloc] initWithObject:image]]; else [ActionStageInstance() nodeRetrieveFailed:path]; } NSString *url1 = url; NSString *url2 = nil; if (cacheFiltered) { url1 = storeUrl; url2 = url; } [cache diskCacheContains:url1 orUrl:url2 completion:^(bool firstInDiskCache, bool secondInDiskCache) { [ActionStageInstance() dispatchOnStageQueue:^ { if (firstInDiskCache || secondInDiskCache) { NSBlockOperation *operation = [[NSBlockOperation alloc] init]; __weak NSOperation *blockOperation = operation; [operation addExecutionBlock:^ { @autoreleasepool { NSOperation *strongBlockOperation = blockOperation; if (strongBlockOperation.isCancelled) return; strongBlockOperation = nil; NSString *cachedUrl = (firstInDiskCache ? url1 : url2); UIImage *cachedImage = nil;//[cache cachedImage:storeUrl availability:TGCacheMemory]; bool imageFromDisc = false; if (cachedImage == nil) { cachedImage = [cache cachedImage:cachedUrl availability:TGCacheDisk]; imageFromDisc = true; } strongBlockOperation = blockOperation; if (strongBlockOperation.isCancelled) return; strongBlockOperation = nil; if (cachedImage != nil) { if (cacheFiltered && cachedImage != nil && processor != nil && firstInDiskCache) { [cachedImage tgPreload]; } else { UIImage *originalImage = cachedImage; if (processor != nil) { cachedImage = processor(cachedImage); if (cacheFiltered && !firstInDiskCache) { NSData *filteredData = UIImageJPEGRepresentation(cachedImage, 0.5f); [cache cacheImage:nil withData:filteredData url:storeUrl availability:TGCacheDisk]; } if (cachedImage == originalImage && imageFromDisc) cachedImage = [cachedImage preloadedImage]; } else if (imageFromDisc) cachedImage = [cachedImage preloadedImage]; } strongBlockOperation = blockOperation; if (strongBlockOperation.isCancelled) return; strongBlockOperation = nil; if (imageFromDisc) { if (allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:cachedImage withData:nil url:storeUrl availability:TGCacheMemory]; if (allowThumbnailCache) [cache cacheThumbnail:cachedImage url:storeUrl]; } strongBlockOperation = blockOperation; if (strongBlockOperation.isCancelled) return; strongBlockOperation = nil; [ActionStageInstance() dispatchOnStageQueue:^ { if (delayFastCompletion && cachedImage.size.width * cachedImage.size.height > 200 * 200) { CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent(); CFAbsoluteTime threshold = cachedImage.size.width * cachedImage.size.height <= 200 * 200 ? minimalThreshold : fastDelayThresold; if (currentTime - lastCompletionTime < threshold) { CFTimeInterval delay = threshold - (currentTime - lastCompletionTime); //TGLog(@"Delay image operation for %f ms", delay * 1000.0); lastCompletionTime = currentTime; usleep((int32_t)(delay * 1000 * 1000)); } else lastCompletionTime = currentTime; } [ActionStageInstance() nodeRetrieved:path node:[[SGraphObjectNode alloc] initWithObject:cachedImage]]; }]; return; } else { [cache removeFromDiskCache:cachedUrl]; [ActionStageInstance() nodeRetrieveFailed:path]; } } }]; operation.threadPriority = imageProcessingPriority(); self.cancelToken = operation; [[TGImageDownloadActor operationQueue] addOperation:operation]; } else { if ([url1 hasPrefix:@"video-thumbnail-"]) { [ActionStageInstance() nodeRetrieveFailed:path]; return; } if ([self.path hasPrefix:@"/img/(download:"]) { NSBlockOperation *processingOperation = [[NSBlockOperation alloc] init]; self.cancelToken = processingOperation; self.downloadCompletionBlock = ^(NSData *imageData) { if (imageData != nil) { __weak NSOperation *weakBlockOperation = processingOperation; [processingOperation addExecutionBlock:^ { @autoreleasepool { NSOperation *blockOperation = weakBlockOperation; if (blockOperation.isCancelled) return; UIImage *image = nil; NSData *data = nil; image = [[UIImage alloc] initWithData:imageData]; data = imageData; if (image == nil || data == nil) [ActionStageInstance() actionFailed:path reason:-1]; else { UIImage *imageForThumbnail = nil; TGImageInfo *imageInfo = [userProperties objectForKey:@"imageInfo"]; if (imageInfo != nil) imageForThumbnail = image; if (image != nil) { [cache cacheImage:nil withData:data url:url availability:TGCacheDisk]; if (processor != nil) { UIImage *originalImage = image; image = processor(image); if (image == originalImage) image = [image preloadedImage]; if (allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:image withData:nil url:storeUrl availability:TGCacheMemory]; if (allowThumbnailCache) [cache cacheThumbnail:image url:storeUrl]; } else { image = [image preloadedImage]; if (allowMemoryCache && (!TG_CACHE_INPLACE || forceMemoryCache)) [cache cacheImage:image withData:nil url:storeUrl availability:TGCacheMemory]; if (allowThumbnailCache) [cache cacheThumbnail:image url:storeUrl]; } if (delayFastCompletion) { CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent(); CFAbsoluteTime threshold = image.size.width * image.size.height <= 180 * 180 ? minimalThreshold : fastDelayThresold; if (currentTime - lastCompletionTime < threshold) { CFTimeInterval delay = threshold - (currentTime - lastCompletionTime); TGLog(@"Delay image operation for %f ms", delay * 1000.0); lastCompletionTime = currentTime; usleep((int32_t)(delay * 1000 * 1000)); } else lastCompletionTime = currentTime; } [ActionStageInstance() dispatchOnStageQueue:^ { [ActionStageInstance() nodeRetrieved:path node:[[SGraphObjectNode alloc] initWithObject:image]]; if (imageInfo != nil && imageForThumbnail != nil && ![url hasPrefix:@"upload"]) { CGSize thumbnailSize = CGSizeZero; NSString *thumbnailUrl = [imageInfo closestImageUrlWithSize:CGSizeMake(90, 90) resultingSize:&thumbnailSize]; if (thumbnailUrl != nil) { thumbnailSize = TGFitSize(CGSizeMake(imageForThumbnail.size.width * imageForThumbnail.scale, imageForThumbnail.size.height * imageForThumbnail.scale), [TGGenericModernConversationCompanion preferredInlineThumbnailSize]); UIImage *thumbnailImage = TGScaleImageToPixelSize(imageForThumbnail, thumbnailSize); if (thumbnailImage != nil) { NSData *thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.85f); if (thumbnailData != nil) { [cache removeFromMemoryCache:thumbnailUrl matchEnd:true]; //TGLog(@"url: %@", url); //TGLog(@"thumbnail url: %@", thumbnailUrl); [cache cacheImage:nil withData:thumbnailData url:thumbnailUrl availability:TGCacheDisk]; TGFileDownloadActor *fileActor = (TGFileDownloadActor *)[ActionStageInstance() executingActorWithPath:[[NSString alloc] initWithFormat:@"/tg/file/(%@)", thumbnailUrl]]; if (fileActor != nil) { [fileActor completeWithData:thumbnailData]; } else { TGDispatchAfter(0.08, [ActionStageInstance() globalStageDispatchQueue], ^ { [ActionStageInstance() dispatchResource:[[NSString alloc] initWithFormat:@"/as/media/imageThumbnailUpdated"] resource:thumbnailUrl]; }); } } } } } if ([[userProperties objectForKey:@"storeAsAsset"] boolValue] && TGAppDelegateInstance.autosavePhotos) { bool shouldSave = true; if (![userProperties[@"forceSave"] boolValue]) { int mid = [userProperties[@"messageId"] intValue]; int64_t conversationId = [userProperties[@"conversationId"] longLongValue]; if (mid != 0 && conversationId != 0) { int minAutosaveMid = [TGDatabaseInstance() minAutosaveMessageIdForConversation:conversationId]; if (mid < minAutosaveMid) shouldSave = false; } } if (shouldSave) { [ActionStageInstance() requestActor:[[NSString alloc] initWithFormat:@"/tg/checkImageStored/(%lu)", (unsigned long)[url hash]] options:[[NSDictionary alloc] initWithObjectsAndKeys:url, @"url", nil] watcher:TGTelegraphInstance]; } } }]; } else { [ActionStageInstance() nodeRetrieveFailed:path]; } } } }]; processingOperation.threadPriority = imageProcessingPriority(); [[TGImageDownloadActor operationQueue] addOperation:processingOperation]; } else { [ActionStageInstance() nodeRetrieveFailed:path]; } }; if (contentHints & TGRemoteImageContentHintLargeFile && userProperties != nil && [[userProperties objectForKey:@"messageId"] intValue] != 0 && [userProperties objectForKey:@"mediaId"] != nil) { int64_t conversationId = [userProperties[@"conversationId"] longLongValue]; int32_t messageId = [[userProperties objectForKey:@"messageId"] intValue]; [[TGDownloadManager instance] enqueueItem:self.path messageId:[[userProperties objectForKey:@"messageId"] intValue] itemId:[userProperties objectForKey:@"mediaId"] groupId:conversationId itemClass:TGDownloadItemClassImage]; _updateMediaAccessTimeOnRelease = true; _messageId = messageId; TGMediaId *mediaId = [userProperties objectForKey:@"mediaId"]; _imageId = mediaId.itemId; } _requestedActors = true; [ActionStageInstance() requestActor:[NSString stringWithFormat:@"/tg/file/(%@)", url] options:[NSDictionary dictionaryWithObjectsAndKeys:url, @"url", nil] watcher:self]; } else { if (![url hasPrefix:@"upload"]) [ActionStageInstance() dispatchMessageToWatchers:self.path messageType:@"progress" message:[[NSNumber alloc] initWithFloat:0.0f]]; _requestedActors = true; [ActionStageInstance() requestActor:[self.path stringByReplacingOccurrencesOfString:@"/img/(" withString:@"/img/(download:"] options:options flags:([[userProperties objectForKey:@"changePriority"] boolValue] ? TGActorRequestChangePriority : 0) watcher:self]; } } }]; }]; } - (void)actorReportedProgress:(NSString *)path progress:(float)progress { if ([path hasPrefix:@"/tg/file/"]) { _progress = progress; [ActionStageInstance() dispatchMessageToWatchers:self.path messageType:@"progress" message:[[NSNumber alloc] initWithFloat:_progress]]; } else if ([path hasPrefix:@"/img/"]) { _progress = progress; [ActionStageInstance() dispatchMessageToWatchers:self.path messageType:@"progress" message:[[NSNumber alloc] initWithFloat:_progress]]; } } - (void)actorMessageReceived:(NSString *)path messageType:(NSString *)messageType message:(id)message { if ([path hasPrefix:@"/img/"]) { if ([messageType isEqualToString:@"progress"]) { _progress = [message floatValue]; [ActionStageInstance() dispatchMessageToWatchers:self.path messageType:messageType message:message]; } } } - (void)watcherJoined:(ASHandle *)watcherHandle options:(NSDictionary *)options waitingInActorQueue:(bool)waitingInActorQueue { [watcherHandle receiveActorMessage:self.path messageType:@"progress" message:[[NSNumber alloc] initWithFloat:_progress]]; [super watcherJoined:watcherHandle options:options waitingInActorQueue:waitingInActorQueue]; } - (void)actorCompleted:(int)resultCode path:(NSString *)path result:(id)result { if ([path hasPrefix:@"/tg/file/"]) { if (resultCode == ASStatusSuccess) { if (self.downloadCompletionBlock) self.downloadCompletionBlock(((SGraphObjectNode *)result).object); } else { [ActionStageInstance() nodeRetrieveFailed:self.path]; self.downloadCompletionBlock = nil; } } else if ([path hasPrefix:@"/img/"]) { if (resultCode == ASStatusSuccess) { [ActionStageInstance() actionCompleted:self.path result:result]; } else { [ActionStageInstance() nodeRetrieveFailed:self.path]; } } } - (void)cancel { if (self.cancelToken != nil) { if ([self.cancelToken isKindOfClass:[NSOperation class]]) [((NSOperation *)self.cancelToken) cancel]; else [TGTelegraphInstance cancelRequestByToken:self.cancelToken]; self.cancelToken = nil; self.downloadCompletionBlock = nil; } [ActionStageInstance() removeWatcher:self]; [super cancel]; } @end