1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-03 09:57:46 +01:00
Telegram/Telegraph/TGImageDownloadActor.m
2016-02-25 01:03:51 +01:00

741 lines
34 KiB
Objective-C

#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