text:
//
//  RAGE_DownloadManager.m
//  RAGE_DownloadManager
//
//  Created by RAGE_Devil_Jameson on 2017/5/3.
//  Copyright © 2017年 RD_Jameson Personal. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "RAGE_DownloadManager.h"
#import "RAGE_DownloadRequest.h"

NSString * const RAGE_BACKGROUND_SESSION_IDENTIFIER = @"RAGE_REQUSET_MANAGER_BACKGROUND_SESSION";
NSString * const RAGE_DownloadManagerURLErrorDomain = @"RAGE_DOWNLOAD_MANAGER_URL_ERROR_DOMAIN";

NSString * const RAGE_DownloadManagerCacheFolder = @"RAGE.DownloadManager/Downloads/Cache/";
NSString * const RAGE_DownloadManagerLibraryFolder = @"RAGE.DownloadManager/Temp/";
NSString * const RAGE_DownloadManagerResumeDataFile = @"rage_resu.me";

NSString * const RAGE_DownloadManagerSavingOperationQueueIdentifier = @"RAGE_OPERATION_SAVING_QUEUE";

NSString * const RAGE_DownloadManagerShouldCleanCacheNotification = @"RAGE.DownloadManager.CleanCache";

NSString * const RAGE_DownloadManagerRequestDidUpdateProgress = @"RAGE.DownloadManager.Request.Progress.Updated";
NSString * const RAGE_DownloadManagerRequestDidFinished = @"RAGE.DownloadManager.Request.Finished";

typedef NS_ENUM(NSInteger, RAGE_ErrorCode){
    RAGE_URLUnsupported = -1,
};

@interface RAGE_DownloadManager () <NSURLSessionDownloadDelegate,NSURLSessionDataDelegate>
@property (nonatomic) NSMutableArray * operations;
@property (nonatomic) NSMutableDictionary * resumableOperations;
@property (nonatomic) NSMutableDictionary <NSNumber*,RAGE_DownloadRequest*> * operations_dict;
@property (nonatomic) NSURLSession * defaultSession;
@property NSOperationQueue * savingOperationQueue;
@property NSMutableDictionary * resumeDataContainer;
@end

#pragma mark -

@implementation RAGE_DownloadManager

+(instancetype)requestManager{
    static RAGE_DownloadManager * manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [RAGE_DownloadManager new];

        NSOperationQueue * dq = [[NSOperationQueue alloc] init];
        [dq setName:RAGE_DownloadManagerSavingOperationQueueIdentifier];
        [dq setMaxConcurrentOperationCount:1];
        manager.savingOperationQueue = dq;

        NSURLSessionConfiguration * backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:RAGE_BACKGROUND_SESSION_IDENTIFIER];
        //-----
        /*
         About Timeouts

         timeoutIntervalForResource default value is 7-days (in sec)
         timeoutIntervalForRequest default value is 60 secs
         */
//        [backgroundConfiguration setTimeoutIntervalForRequest:15];
//        [backgroundConfiguration setTimeoutIntervalForResource:30];

        NSURLSession * session = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:manager delegateQueue:nil];

        [manager setDefaultSession:session];

        NSFileManager * fm = [NSFileManager defaultManager];

        NSURL * cacheURL = [fm URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;

        NSURL * folderURL = [cacheURL URLByAppendingPathComponent:RAGE_DownloadManagerCacheFolder];

        NSURL * libURL = [fm URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];

        BOOL t = YES;
        BOOL exists = [fm fileExistsAtPath:folderURL.path isDirectory:&t];
        if (!exists)
        {
            [fm createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:nil];
        }

        if (![fm fileExistsAtPath:[libURL URLByAppendingPathComponent:RAGE_DownloadManagerResumeDataFile].path]) {
            [fm createFileAtPath:[libURL URLByAppendingPathComponent:RAGE_DownloadManagerResumeDataFile].path contents:nil attributes:nil];
        }

        [[NSNotificationCenter defaultCenter] addObserver:manager selector:@selector(clearCaches) name:RAGE_DownloadManagerShouldCleanCacheNotification object:nil];

        manager.resumeDataContainer = [NSMutableDictionary dictionary];
        [manager loadManualSuspendedOperations];

    });
    return manager;
}

#pragma mark -
#pragma mark Checking Methods

/**
 Check if url was valid, current support url should have prefix 'http' / 'https'

 @param url target url
 @return if valid, return YES, else NO
 */
-(BOOL)checkURL:(NSURL*)url {
    NSString* string = [NSString stringWithFormat:@"%@",url];
    if ([string hasPrefix:@"http"] || [string hasPrefix:@"https"])
    {
        return YES;
    }
    return NO;
}

#pragma mark -
#pragma mark Adding Operation Methods

/**
 Adding single download request, identifier must be unique and nonnull.

 @param url target url
 @param identifier request's identifier
 @param error if not success, will return error message, may be nil
 @return result
 */
-(BOOL)addDownloadRequestWithURL:(nonnull NSURL *)url withIdentifier:(nonnull NSString *)identifier error:(NSError *__autoreleasing *)error {
    BOOL valid = [self checkURL:url];
    if (!valid )
    {
        if (error != NULL)
        {

        NSError * nsErr = [NSError errorWithDomain:RAGE_DownloadManagerURLErrorDomain
                                              code:RAGE_URLUnsupported
                                          userInfo:@{NSLocalizedFailureReasonErrorKey:@"URL Not Supported",
                                                     NSLocalizedDescriptionKey : @"Target URL Should be http/https"}];
        *error = nsErr;
        }
        return NO;
    }

    RAGE_DownloadRequest * request = [RAGE_DownloadRequest downloadRequestWithURL:url identifier:identifier owner:self];
    [self.operations_dict setObject:request forKey:[NSNumber numberWithUnsignedInteger:request.taskIdentifier]];

    [request resume];

    [self triggerNetworkActivityIndicatorIfNeeded];

    return YES;
}

/**
 Adding multiple download requests by passing [URL] and [String] identifiers, the number of [URL] and [String] should be equal.

 @param urls Target urls
 @param identifiers Request's identifiers
 @param handler if any of the target URL was invalid, invalid urls will callback in handler
 @return operation status
 */
-(BOOL)addDownloadRequestsWithURLs:(nonnull NSArray<NSURL *> *)urls withIdentifiers:(nonnull NSArray<NSString *> *)identifiers completeHandler:(requestCompleteHandler)handler {
    __weak typeof (self) weak_self = self;
    __block NSMutableArray * possibleFailedURLs = [NSMutableArray array];
    [urls enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSURL * _Nonnull target, NSUInteger idx, BOOL * _Nonnull stop)
    {
        BOOL valid = [weak_self checkURL:target];
        if (valid)
        {
            NSString * identifier = [identifiers objectAtIndex:idx];
            [weak_self addDownloadRequestWithURL:target withIdentifier:identifier error:nil];
        }
        else
        {
            [possibleFailedURLs addObject:target];
        }
    }];
    handler(possibleFailedURLs.copy);
    return YES;
}

#pragma mark -
#pragma mark Suspend Operation Methods

/**
 Suspend all operations

 @return result
 */
-(BOOL)suspendAllDownloadRequest {
    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber* _Nonnull key, RAGE_DownloadRequest* _Nonnull request, BOOL * _Nonnull stop)
    {
        [request suspend];
    }];
    return YES;
}

/**
 Suspend target operation with request's identifier

 @param requestIdentifier unique identifier, if multiple request was using the same identifier, only first operation captured will be suspended
 @return result
 */
-(BOOL)suspendDownloadRequest:(nonnull NSString *)requestIdentifier {
    __block BOOL suspended = NO;

    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull request, BOOL * _Nonnull stop) {
        if ([request.identifier isEqualToString:requestIdentifier])
        {
            [request suspend];
            *stop = YES;
            suspended = YES;
        }
    }];
    return suspended;
}

#pragma mark -
#pragma mark Cancel Operation Methods
#pragma mark Non-resumable Canceling
/**
 Cancel all operations, without producing resume data

 @return result
 */
-(BOOL)cancelAllDownloadRequestWithoutProducingResumeData {
    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber*  _Nonnull key, RAGE_DownloadRequest*  _Nonnull request, BOOL * _Nonnull stop) {
        [request cancel];
    }];
    return YES;
}

/**
 Cancel target operation by given identifier, without

 @param requestIdentifier unique identifier, if multiple request was using the same identifier, only first operation captured will be cancel
 @return result
 */
-(BOOL)cancelDownloadRequest:(nonnull NSString *)requestIdentifier {
    __block BOOL canceled = NO;

    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull request, BOOL * _Nonnull stop) {
        if ([request.identifier isEqualToString:requestIdentifier])
        {
            [request cancel];
            *stop = YES;
            canceled = YES;
        }
    }];
    return canceled;

}

#pragma mark Resumable Canceling

/**
 Cancel all operations and producing resume data.

 @return result
 */
-(BOOL)cancelAllDownloadRequestByProducingResumeData {
    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber*  _Nonnull key, RAGE_DownloadRequest*  _Nonnull request, BOOL * _Nonnull stop) {
        [request cancelByProducingResumeData:^(NSData *resumeData) {
            [request updateResumeData:resumeData];
            NSLog(@"Request was canceled with resume data\n %zd bytes",resumeData.length);
            [self.resumableOperations setObject:request forKey:request.identifier];

        }];
    }];
    return YES;
}

/**
 Cancel target operation by given identifier

 @param requestIdentifier unique identifier, if multiple request was using the same identifier, only first operation captured will be cancel
 @return result
 */
-(BOOL)cancelDownloadRequestByProducingResumeData:(nonnull NSString *)requestIdentifier {
    __block BOOL canceled = NO;

    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull request, BOOL * _Nonnull stop) {
        if ([request.identifier isEqualToString:requestIdentifier])
        {
            [request cancelByProducingResumeData:^(NSData *resumeData) {
                [request updateResumeData:resumeData];
                NSLog(@"Request was canceled with resume data\n %zd bytes",resumeData.length);
                [self.resumableOperations setObject:request forKey:request.identifier];
            }];
            *stop = YES;
            canceled = YES;
        }
    }];
    return canceled;
}

#pragma mark -
#pragma mark Resume Methods

/**
 Resume a suspended request by given identifier

 @param identifier unique identifier
 @return result
 */
-(BOOL)resumeDownloadRequestWithIdentifier:(nonnull NSString *)identifier {
    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull obj, BOOL * _Nonnull stop) {
        if ([obj.identifier isEqualToString:identifier])
        {
            if (obj.state == NSURLSessionTaskStateSuspended) {
                [obj resume];
                *stop = YES;
            }
        }
    }];
    return YES;
}

/**
 Resume download request by request, request will attempt to finding resume data if possible

 @param request request which want to resume
 @return result
 */
-(BOOL)resumeDownloadRequest:(RAGE_DownloadRequest *)request {
    if (!request.isResumable) {return NO;}
    [request reassignManager:self];
    [request resume];
    [self.operations_dict setObject:request forKey:[NSNumber numberWithUnsignedInteger:request.taskIdentifier]];
    return YES;
}

#pragma mark-
#pragma mark Request Current Operations

/**
 Request current all download requests

 @return possible all request
 */
-(nullable NSDictionary *)requestCurrentOperations {
    NSArray * values = self.operations_dict.allValues;
    NSMutableDictionary * ops = [NSMutableDictionary dictionaryWithCapacity:values.count];
    for (NSInteger idx = 0; idx < values.count; idx++)
    {
        RAGE_DownloadRequest * request = [values objectAtIndex:idx];
        [ops setObject:request forKey:request.identifier];
    }
    return ops;
}

/**
 Request current all donwload requests by given complete handler

 @param handler handler contained possible request
 */
-(void)requestCurrentOperationsWithCompleteHandler:(requestOperationsCompleteHandler)handler {
    __block NSInteger counter = 0;
    NSInteger trigger = self.operations_dict.count;
    __block NSMutableDictionary * res = [NSMutableDictionary dictionaryWithCapacity:trigger];
    [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull obj, BOOL * _Nonnull stop) {
        [res setObject:obj forKey:obj.identifier];
        counter+=1;
        if (counter == trigger-1) {
            handler(res);
        }
    }];
}

#pragma mark -
#pragma mark NSURLSessionDownloadDelegate
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    RAGE_DownloadRequest * request = [self.operations_dict objectForKey:[NSNumber numberWithUnsignedInteger:downloadTask.taskIdentifier]];
    NSHTTPURLResponse * response = (NSHTTPURLResponse*)downloadTask.response;
    [request setStatusCode:response.statusCode];

    if (request.identifier == nil) {return;}

    Float64 progress = totalBytesWritten * 1.0f / totalBytesExpectedToWrite * 1.0f;
    [request updateProgress:progress];

//#warning Suspended And Resumable Debug
//    if (totalBytesWritten * 1.0f > totalBytesExpectedToWrite * 0.1f)
//    {
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//            [request suspend];
//    });
//    }

    // Delegate Callback
    if ([self.delegate respondsToSelector:@selector(manager:requestProgressDidUpdate:)]) {
        [self.delegate manager:self requestProgressDidUpdate:request];
    }else{
        NSLog(@"Delegate Not Responding To manager:requestProgressDidUpdate:\n|Notification Posted");
        [[NSNotificationCenter defaultCenter] postNotificationName:RAGE_DownloadManagerRequestDidUpdateProgress object:self userInfo:@{@"request":request}];
    }

}

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{

    if (!location) {return;}
    dispatch_sync(dispatch_get_main_queue(), ^{

        NSFileManager * fm = [NSFileManager defaultManager];

        NSURL * cacheURL = [fm URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;

        NSURL * folderURL = [cacheURL URLByAppendingPathComponent:RAGE_DownloadManagerCacheFolder];

//        NSURL * fileURL = [folderURL URLByAppendingPathComponent:downloadTask.originalRequest.URL.lastPathComponent];
        NSURL * fileURL = [folderURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%zd.%@",arc4random(),downloadTask.originalRequest.URL.lastPathComponent.pathExtension]];

        RAGE_DownloadRequest * request = [self.operations_dict objectForKey:[NSNumber numberWithUnsignedInteger:downloadTask.taskIdentifier]];

        NSError * nsErr;
        //        [[NSFileManager defaultManager] copyItemAtURL:location toURL:tmpUrl error:&nsErr];

        if ([fm fileExistsAtPath:fileURL.path]) {
            [fm removeItemAtURL:fileURL error:nil];
        }

        [fm moveItemAtURL:location toURL:fileURL error:&nsErr];
        if (nsErr)
        {
            NSLog(@"Error : %@",nsErr);
        }
        [request updateCachePosition:fileURL];
    });
}

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
    NSLog(@"downloadTask:%@ didResumeAtOffet:%zd expectedTotalBytes:%zd",downloadTask,fileOffset,expectedTotalBytes);
}

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    __weak typeof (self) weak_self = self;
    if ([task isKindOfClass:[NSURLSessionDownloadTask class]])
    {
        RAGE_DownloadRequest * request = [self.operations_dict objectForKey:[NSNumber numberWithUnsignedInteger:task.taskIdentifier]];
        //  200 - 207 Success Status Code, if Beyond Range, Handle As Error
        if (error || request.statusCode < 200 || request.statusCode > 207) {
            // Request Exception
            [self handleExceptionRequest:request WithErrorMessage:error];
//            return;
        }

        [self.operations_dict removeObjectForKey:[NSNumber numberWithUnsignedInteger:request.taskIdentifier]];

        if ([self.delegate respondsToSelector:@selector(manager:request:DidFinishedWithError:)]) {
            [self.delegate manager:weak_self request:request DidFinishedWithError:error];
        }
        else {
            NSMutableDictionary * userinfo = [NSMutableDictionary dictionaryWithCapacity:1];

            if (request)
            {
                [userinfo setObject:request forKey:@"request"];
            }

            if (error)
            {
                [userinfo setObject:error forKey:@"error"];
            }
            NSLog(@"Delegate Not Responding To manager:request:DidFinishedWithError:\n|Notification Posted");
            [[NSNotificationCenter defaultCenter] postNotificationName:RAGE_DownloadManagerRequestDidFinished object:self userInfo:userinfo];
        }

    }
    [self triggerNetworkActivityIndicatorIfNeeded];
}

#pragma mark -
#pragma mark Function plugins

/**
 Handle request exceptions

 @param request Exception request
 @param error Possible error message
 */
-(void)handleExceptionRequest:(RAGE_DownloadRequest*)request WithErrorMessage:(NSError*)error {
    NSErrorDomain errorDomain = error.domain;
    if ([errorDomain isEqualToString:NSURLErrorDomain]) {
        switch (error.code)
        {
                case -1:
            {
                // Timeout
                NSLog(@"The request timed out");
            }
                break;

            case -1002:
            {
                // URL Unsupported
                NSLog(@"unsupported URL");
            }
                break;

                case -999:
            {
                // Canceled By User | Force Quit Callback
                if (_resumeDataContainer)
                {
                    _resumeDataContainer = [NSMutableDictionary dictionary];
                }

                NSData * possibleResumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
                NSURL * failingError = [error.userInfo objectForKey:NSURLErrorFailingURLErrorKey];

                if (possibleResumeData && failingError)
                {
                    [self.resumeDataContainer setObject:possibleResumeData forKey:failingError];
                    NSFileManager * fm = [NSFileManager defaultManager];
                    NSURL * libURL = [fm URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
                    [NSKeyedArchiver archiveRootObject:self.resumeDataContainer toFile:[libURL URLByAppendingPathComponent:RAGE_DownloadManagerResumeDataFile].path];
                }

            }
                break;
            default:
                break;
        }
    }
    if (request.statusCode > 207) {
    }

    //-----Acording to protocol of HTTP/1.0, there is no status code of 1xx, server should not send such status code
    if (request.statusCode < 200) {
    }

    [self removeRequest:request];
}

/**
 Remove request from current operations

 @param request Target request want to removed
 */
-(void)removeRequest:(RAGE_DownloadRequest*)request {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.operations_dict removeObjectForKey:[NSNumber numberWithUnsignedInteger:request.taskIdentifier]];
//        NSLog(@"Current Operations \n %@",self.operations_dict);
    });
}

/**
 Clean download caches, will called after manager was dealloced
 */
-(void)clearCaches {
    NSFileManager * fm = [NSFileManager defaultManager];
    NSURL * cacheURL = [fm URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;
    NSURL * targetURL = [cacheURL URLByAppendingPathComponent:RAGE_DownloadManagerCacheFolder];

    NSArray <NSURL*> * possibleCaches = [fm contentsOfDirectoryAtURL:targetURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];

    [possibleCaches enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSURL * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//        dispatch_async(dispatch_get_main_queue(), ^{
            [fm removeItemAtURL:obj error:nil];
//        });
    }];
}

/**
 Cancel And Saving Current Suspended And Resumable Operations
 */
-(void)savingOperation {
    // This method will overwrite the operations that already exists
    __weak typeof (self) weak_self = self;
//        __block NSMutableDictionary * odic = [NSMutableDictionary dictionary];

    NSInteger trigger = self.operations_dict.count;
    __block NSInteger counter = 0;
        [self.operations_dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSNumber * _Nonnull key, RAGE_DownloadRequest * _Nonnull obj, BOOL * _Nonnull stop) {
            [obj cancelByProducingResumeData:^(NSData *resumeData) {
                [obj updateResumeData:resumeData];
                [weak_self.resumableOperations setObject:obj forKey:obj.identifier];
                counter += 1;
                NSLog(@"Counting : %zd Trigger : %zd",counter,trigger);
                if (counter >= trigger)
                {

                    [weak_self performSavingAction:weak_self.resumableOperations];
                }

            }];
        }];
}

/**
 Actural perform saving action [should not call this method manually]

 @param dict suspended operations dict
 */
-(void)performSavingAction:(NSDictionary*)dict {
    NSLog(@"Performing Saving Action");
    NSURL * file = [self archivedURL];
    [NSKeyedArchiver archiveRootObject:dict toFile:file.path];
}

/**
 Unarchived last time suspended operations

 @return operations suspended and resumable, may be nil
 */
-(NSDictionary <NSString*,RAGE_DownloadRequest*> * _Nullable )requestResumableOperations {
    NSURL * file = [self archivedURL];
    NSDictionary * dic = [NSKeyedUnarchiver unarchiveObjectWithFile:file.path];
    return dic;
}

/**
 Request archived URL (Resumable Operations Plugin)

 @return File url
 */
-(NSURL*)archivedURL {
    NSFileManager * fm = [NSFileManager defaultManager];

    NSURL * lib = [fm URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask].firstObject;
    NSURL * subFolder = [lib URLByAppendingPathComponent:RAGE_DownloadManagerLibraryFolder];
    NSURL * archivedFile = [subFolder URLByAppendingPathComponent:@"suspendedOperations.rage"];

    BOOL isFolder = YES;
    BOOL folderIsExists = [fm fileExistsAtPath:subFolder.path isDirectory:&isFolder];
    if (!folderIsExists) {
        [fm createDirectoryAtURL:subFolder withIntermediateDirectories:YES attributes:nil error:nil];
    }

    return archivedFile;

}

/**
 If you really don't want to assign requests' identifier by yourself, however, manager also provide a method to generate a random and nearly unique identifier

 @return (most of time) unique identifier
 */
-(nonnull NSString *)requestUniqueIdentifier {
    NSMutableString * uniqueIdentifier = @"RAGE".mutableCopy;

    NSUInteger alpha = arc4random_uniform(256);
    NSUInteger bravo = arc4random_uniform(256);
    NSUInteger charlie = arc4random_uniform(256);
    NSUInteger delta = arc4random_uniform(256);
    NSUInteger echo = arc4random_uniform(256);
    NSUInteger foxtrot = arc4random_uniform(256);

    [uniqueIdentifier appendFormat:@"-%zd",alpha];
    [uniqueIdentifier appendFormat:@"-%zd",bravo];
    [uniqueIdentifier appendFormat:@"-%zd",charlie];
    [uniqueIdentifier appendFormat:@"-%zd",delta];
    [uniqueIdentifier appendFormat:@"-%zd",echo];
    [uniqueIdentifier appendFormat:@"-%zd",foxtrot];

    return uniqueIdentifier;
}

/**
 Load all possible resumedatas from local file
 */
-(void)loadManualSuspendedOperations {
    NSFileManager * fm = [NSFileManager defaultManager];
    NSURL * libURL = [fm URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    NSString * target = [libURL URLByAppendingPathComponent:RAGE_DownloadManagerResumeDataFile].path;
    NSDictionary * possibleDict = [NSKeyedUnarchiver unarchiveObjectWithFile:target];
    if (possibleDict)
    {
        self.resumeDataContainer = possibleDict.mutableCopy;
    }
}

/**
 Trigger network activity indicator if there is any request running
 */
-(void)triggerNetworkActivityIndicatorIfNeeded {

    BOOL visible = NO;
    for (RAGE_DownloadRequest * request in self.operations_dict)
    {
        if (request.state == NSURLSessionTaskStateRunning) {
            visible = YES;
            break;
        }
    }

    BOOL isDisplaying = [[UIApplication sharedApplication] isNetworkActivityIndicatorVisible];

    if (visible && !isDisplaying)
    {
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
        return;
    }

    if (!visible && isDisplaying)
    {
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        return;
    }
}

/**
 Request possible resumedata with request url

 @param url request url, usually download url
 @return possible resumedata, may be nil
 */
-(NSData * _Nullable)requestResumeDataWithURL:(NSURL * _Nonnull)url {
    NSData * possibleData = [self.resumeDataContainer objectForKey:url];
    if (possibleData)
    {
        [self.resumeDataContainer removeObjectForKey:url];
    }
    return possibleData;
}

/**
 Update operation_dict with taskIdentifier which was resumed from resumedata

 @param request request need to be assign to a new key
 */
-(void)updateRequestContent:(RAGE_DownloadRequest *)request {
    for (NSNumber * key in self.operations_dict)
    {
        RAGE_DownloadRequest * pre_request = [self.operations_dict objectForKey:key];
        if ([request.requestURL.absoluteString isEqualToString:pre_request.requestURL.absoluteString])
        {
            // add current request to opertaion_dict
            [self.operations_dict setObject:request forKey:[NSNumber numberWithUnsignedInteger:request.taskIdentifier]];
            // remove previous request
            [self.operations_dict removeObjectForKey:key];
            break;
        }
    }
}

#pragma mark -
#pragma mark Lazy let
-(NSMutableArray *)operations {
    if (!_operations)
    {
        _operations = [NSMutableArray array];
    }
    return _operations;
}
-(NSMutableDictionary *)operations_dict {
    if (!_operations_dict)
    {
        _operations_dict = [NSMutableDictionary dictionary];
    }
    return _operations_dict;
}
-(NSMutableDictionary *)resumableOperations {
    if (!_resumableOperations)
    {
        _resumableOperations = [NSMutableDictionary dictionary];
    }
    return _resumableOperations;
}

#pragma mark -
#pragma mark Dealloc
-(void)dealloc{
    [self suspendAllDownloadRequest];
    [self clearCaches];
    [self.operations_dict removeAllObjects];
    [self.defaultSession invalidateAndCancel];
    NSLog(@"Manager Dealloced");
}

@end


text:
package android.os.cts;

public class OSFeatures {
    static {
        System.loadLibrary("cts_jni");
    }

    public static native int getNoNewPrivs();
    public static native int prctlCapBsetRead(int i);
}

redpajama_set_name: RedPajamaGithub


text:
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.facebook.presto.plugin.geospatial;

import com.facebook.presto.common.block.BlockBuilder;
import com.facebook.presto.common.type.StandardTypes;
import com.facebook.presto.spi.function.AggregationFunction;
import com.facebook.presto.spi.function.CombineFunction;
import com.facebook.presto.spi.function.InputFunction;
import com.facebook.presto.spi.function.OutputFunction;
import com.facebook.presto.spi.function.SqlType;
import io.airlift.slice.Slice;

import static com.facebook.presto.plugin.geospatial.GeometryType.GEOMETRY_TYPE_NAME;
import static com.facebook.presto.plugin.geospatial.SpatialPartitioningAggregateFunction.NAME;

@AggregationFunction(value = NAME, decomposable = false)
public class SpatialPartitioningAggregateFunction
{
    public static final String NAME = "spatial_partitioning";

    private SpatialPartitioningAggregateFunction() {}

    @InputFunction
    public static void input(SpatialPartitioningState state, @SqlType(GEOMETRY_TYPE_NAME) Slice slice)
    {
        throw new UnsupportedOperationException("spatial_partitioning(geometry, samplingPercentage) aggregate function should be re-written into spatial_partitioning(geometry, samplingPercentage, partitionCount)");
    }

    @CombineFunction
    public static void combine(SpatialPartitioningState state, SpatialPartitioningState otherState)
    {
        throw new UnsupportedOperationException("spatial_partitioning(geometry, samplingPercentage) aggregate function should be re-written into spatial_partitioning(geometry, samplingPercentage, partitionCount)");
    }

    @OutputFunction(StandardTypes.VARCHAR)
    public static void output(SpatialPartitioningState state, BlockBuilder out)
    {
        throw new UnsupportedOperationException("spatial_partitioning(geometry, samplingPercentage) aggregate function should be re-written into spatial_partitioning(geometry, samplingPercentage, partitionCount)");
    }
}


text:
package com.solertium.util.gwt.charts.example;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.user.client.ui.Image;
import com.googlecode.charts4j.Color;
import com.googlecode.charts4j.GCharts;
import com.googlecode.charts4j.LegendPosition;
import com.googlecode.charts4j.PieChart;
import com.googlecode.charts4j.Slice;

/**
 * Charts4JPieChartExample.java
 *
 * Attempt at an implementation based on the Charts4j GWT
 * project.  This does not depend on any of the code in
 * Google Charts for GWT.
 *
 * @author Carl Scott, Solertium Corporation
 */
public class Charts4JPieChartExample extends SimplePieChartExample {

	@Override
	public Image getChart() {
		/*
		final String[] sampleNames = new String[] {
			"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
		};
		*/
		final Color[] sampleColors = new Color[] {
			Color.RED, Color.BLUE,  Color.GREEN, Color.YELLOW, Color.BLACK
		};
		final List<Double> dataSet = getSampleDataSet();
		final List<Slice> slices = new ArrayList<Slice>();

		double total = 0;
		for (Double value : dataSet)
			total += value;

		for (int i = 0; i < dataSet.size(); i++) {
			int normalizedPercentage = (int)((dataSet.get(i) / total) * 100);
			slices.add(Slice.newSlice(normalizedPercentage, sampleColors[i],"",""));
		}

		final PieChart chart = GCharts.newPieChart(slices);
		chart.setTitle("Weekly Sales");
		chart.setSize(500, 500);
		chart.setLegendPosition(LegendPosition.RIGHT);

		return new Image(chart.toURLString());
	}

	public String getSource() {
		return "final String[] sampleNames = new String[] {\n"+
		"	\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"\n"+
		"};\n"+
		"final Color[] sampleColors = new Color[] {\n"+
		"	Color.RED, Color.BLUE,  Color.GREEN, Color.YELLOW, Color.BLACK\n"+
		"};\n"+
		"final List<Double> dataSet = getSampleDataSet();\n"+
		"final List<Slice> slices = new ArrayList<Slice>();\n\n"+

		"double total = 0;\n"+
		"for (Double value : dataSet)\n"+
		"	total += value;\n\n"+

		"for (int i = 0; i < dataSet.size(); i++) {\n"+
		"	int normalizedPercentage = (int)((dataSet.get(i) / total) * 100);\n"+
		"	slices.add(Slice.newSlice(normalizedPercentage, sampleColors[i]));\n"+
		"}\n\n"+

		"final PieChart chart = GCharts.newPieChart(slices);\n"+
		"chart.setTitle(\"Weekly Sales\");\n"+
		"chart.setSize(500, 500);\n\n"+

		"return new Image(chart.toURLString());";
	}

}


text:
from __future__ import absolute_import, division, print_function

import pretend
import pytest

from symantecssl.auth import SymantecAuth
from symantecssl.core import Symantec
from symantecssl.session import SymantecSession

@pytest.fixture
def api():
    return Symantec("user", "p@ssw0rd")

class TestSymantec:

    def test_basic_init(self):
        api = Symantec("user", "p@ssw0rd")
        assert api.url == "https://api.geotrust.com/webtrust/partner"
        assert isinstance(api.session, SymantecSession)
        assert isinstance(api.session.auth, SymantecAuth)
        assert api.session.auth.username == "user"
        assert api.session.auth.password == "p@ssw0rd"

    def test_init_with_url(self):
        api = Symantec("user", "p@ssw0rd", url="https://api.example.com/")
        assert api.url == "https://api.example.com/"

    def test_submit(self, api):
        resp = pretend.stub(
            content=pretend.stub(),
            raise_for_status=pretend.call_recorder(lambda: None),
        )
        api.session = pretend.stub(
            post=pretend.call_recorder(lambda *a, **k: resp),
        )

        serialized = pretend.stub()
        results = pretend.stub()
        obj = pretend.stub(
            serialize=pretend.call_recorder(lambda: serialized),
            response=pretend.call_recorder(lambda data: results),
        )

        assert api.submit(obj) is results
        assert api.session.post.calls == [
            pretend.call(
                "https://api.geotrust.com/webtrust/partner",
                serialized,
            ),
        ]
        assert resp.raise_for_status.calls == [pretend.call()]
        assert obj.serialize.calls == [pretend.call()]
        assert obj.response.calls == [pretend.call(resp.content)]

    def test_context_manager(self):
        api = Symantec("user", "p@ssw0rd")
        api.close = pretend.call_recorder(lambda: None)

        with api as ctx_api:
            assert ctx_api is api

        assert api.close.calls == [pretend.call()]

    def test_session_close(self):
        api = Symantec("user", "p@ssw0rd")
        api.session = pretend.stub(close=pretend.call_recorder(lambda: None))
        api.close()

        assert api.session.close.calls == [pretend.call()]

    @pytest.mark.parametrize(
        "method",
        [
            "order",
            "get_order_by_partner_order_id",
            "get_orders_by_date_range",
            "get_modified_orders",
            "change_approver_email",
            "reissue",
            "get_quick_approver_list",
            "revoke",
            "modify_order",
            "validate_order_parameters",
            "resend_email",
        ],
    )
    def test_api_methods(self, method, api):
        instance = pretend.stub()
        results = pretend.stub()
        class_ = pretend.call_recorder(lambda **kw: instance)
        api.submit = pretend.call_recorder(lambda o: results)

        setattr(api, "{0}_class".format(method), class_)

        assert getattr(api, method)(foo="bar") is results
        assert class_.calls == [pretend.call(foo="bar")]
        assert api.submit.calls == [pretend.call(instance)]


text:
class AddDeletedAtToSalePrice < SolidusSupport::Migration[4.2]
  def change
    add_column :spree_sale_prices, :deleted_at, :datetime
    add_index :spree_sale_prices, :deleted_at
  end
end


text:
from lymph.core.container import create_container
from lymph.core.interfaces import Interface

class ClientInterface(Interface):
    def should_register(self):
        return False

class Client(object):
    def __init__(self, container, interface=ClientInterface):
        self.container = container
        self.interface = container.install_interface(interface, name='_client')

    @classmethod
    def from_config(cls, config, **kwargs):
        interface_cls = kwargs.pop('interface_cls', ClientInterface)
        container = create_container(config)
        client = cls(container, interface_cls)
        container.start(register=False)
        return client

    def __getattr__(self, name):
        # FIXME: explicit is better than implicit
        return getattr(self.interface, name)


text:
<?php

namespace Symfony\Component\Serializer;

use Symfony\Component\Serializer\Encoder\ChainDecoder;
use Symfony\Component\Serializer\Encoder\ChainEncoder;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;

/**
 * Serializer serializes and deserializes data
 *
 * objects are turned into arrays by normalizers
 * arrays are turned into various output formats by encoders
 *
 * $serializer->serialize($obj, 'xml')
 * $serializer->decode($data, 'xml')
 * $serializer->denormalize($data, 'Class', 'xml')
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Lukas Kahwe Smith <smith@pooteeweet.org>
 */
class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
{
    protected $encoder;
    protected $decoder;
    protected $normalizers       = array();
    protected $normalizerCache   = array();
    protected $denormalizerCache = array();

    public function __construct(array $normalizers = array(), array $encoders = array())
    {
        foreach ($normalizers as $normalizer) {
            if ($normalizer instanceof SerializerAwareInterface) {
                $normalizer->setSerializer($this);
            }
        }
        $this->normalizers = $normalizers;

        $decoders = array();
        $realEncoders = array();
        foreach ($encoders as $encoder) {
            if ($encoder instanceof SerializerAwareInterface) {
                $encoder->setSerializer($this);
            }
            if ($encoder instanceof DecoderInterface) {
                $decoders[] = $encoder;
            }
            if ($encoder instanceof EncoderInterface) {
                $realEncoders[] = $encoder;
            }
        }
        $this->encoder = new ChainEncoder($realEncoders);
        $this->decoder = new ChainDecoder($decoders);
    }

    /**
     * {@inheritdoc}
     */
    final public function serialize($data, $format, array $context = array())
    {
        if (!$this->supportsEncoding($format)) {
            throw new UnexpectedValueException(sprintf('Serialization for the format %s is not supported', $format));
        }

        if ($this->encoder->needsNormalization($format)) {
            $data = $this->normalize($data, $format, $context);
        }

        return $this->encode($data, $format, $context);
    }

    /**
     * {@inheritdoc}
     */
    final public function deserialize($data, $type, $format, array $context = array())
    {
        if (!$this->supportsDecoding($format)) {
            throw new UnexpectedValueException(sprintf('Deserialization for the format %s is not supported', $format));
        }

        $data = $this->decode($data, $format, $context);

        return $this->denormalize($data, $type, $format, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function normalize($data, $format = null, array $context = array())
    {
        if (null === $data || is_scalar($data)) {
            return $data;
        }
        if (is_object($data) && $this->supportsNormalization($data, $format)) {
            return $this->normalizeObject($data, $format, $context);
        }
        if ($data instanceof \Traversable) {
            $normalized = array();
            foreach ($data as $key => $val) {
                $normalized[$key] = $this->normalize($val, $format, $context);
            }

            return $normalized;
        }
        if (is_object($data)) {
            return $this->normalizeObject($data, $format, $context);
        }
        if (is_array($data)) {
            foreach ($data as $key => $val) {
                $data[$key] = $this->normalize($val, $format, $context);
            }

            return $data;
        }
        throw new UnexpectedValueException(sprintf('An unexpected value could not be normalized: %s', var_export($data, true)));
    }

    /**
     * {@inheritdoc}
     */
    public function denormalize($data, $type, $format = null, array $context = array())
    {
        return $this->denormalizeObject($data, $type, $format, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsNormalization($data, $format = null)
    {
        try {
            $this->getNormalizer($data, $format);
        } catch (RuntimeException $e) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        try {
            $this->getDenormalizer($data, $type, $format = null);
        } catch (RuntimeException $e) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    private function getNormalizer($data, $format = null)
    {
        foreach ($this->normalizers as $normalizer) {
            if ($normalizer instanceof NormalizerInterface
                && $normalizer->supportsNormalization($data, $format)
            ) {
                return $normalizer;
            }
        }

        throw new RuntimeException(sprintf('No normalizer found for format "%s".', $format));
    }

    /**
     * {@inheritdoc}
     */
    private function getDenormalizer($data, $type, $format = null)
    {
        foreach ($this->normalizers as $normalizer) {
            if ($normalizer instanceof DenormalizerInterface
                && $normalizer->supportsDenormalization($data, $type, $format)
            ) {
                return $normalizer;
            }
        }

        throw new RuntimeException(sprintf('No denormalizer found for format "%s".', $format));
    }

    /**
     * {@inheritdoc}
     */
    final public function encode($data, $format, array $context = array())
    {
        return $this->encoder->encode($data, $format, $context);
    }

    /**
     * {@inheritdoc}
     */
    final public function decode($data, $format, array $context = array())
    {
        return $this->decoder->decode($data, $format, $context);
    }

    /**
     * Normalizes an object into a set of arrays/scalars
     *
     * @param object $object object to normalize
     * @param string $format format name, present to give the option to normalizers to act differently based on formats
     * @param array $context The context data for this particular normalization
     *
     * @return array|scalar
     *
     * @throws LogicException
     * @throws UnexpectedValueException
     */
    private function normalizeObject($object, $format = null, array $context = array())
    {
        if (!$this->normalizers) {
            throw new LogicException('You must register at least one normalizer to be able to normalize objects.');
        }

        $class = get_class($object);
        if (isset($this->normalizerCache[$class][$format])) {
            return $this->normalizerCache[$class][$format]->normalize($object, $format, $context);
        }

        foreach ($this->normalizers as $normalizer) {
            if ($normalizer->supportsNormalization($object, $format)) {
                $this->normalizerCache[$class][$format] = $normalizer;

                return $normalizer->normalize($object, $format, $context);
            }
        }

        throw new UnexpectedValueException(sprintf('Could not normalize object of type %s, no supporting normalizer found.', $class));
    }

    /**
     * Denormalizes data back into an object of the given class
     *
     * @param mixed  $data   data to restore
     * @param string $class  the expected class to instantiate
     * @param string $format format name, present to give the option to normalizers to act differently based on formats
     * @param array $context The context data for this particular denormalization
     *
     * @return object
     *
     * @throws LogicException
     * @throws UnexpectedValueException
     */
    private function denormalizeObject($data, $class, $format = null, array $context = array())
    {
        if (!$this->normalizers) {
            throw new LogicException('You must register at least one normalizer to be able to denormalize objects.');
        }

        if (isset($this->denormalizerCache[$class][$format])) {
            return $this->denormalizerCache[$class][$format]->denormalize($data, $class, $format, $context);
        }

        foreach ($this->normalizers as $normalizer) {
            if ($normalizer->supportsDenormalization($data, $class, $format)) {
                $this->denormalizerCache[$class][$format] = $normalizer;

                return $normalizer->denormalize($data, $class, $format, $context);
            }
        }

        throw new UnexpectedValueException(sprintf('Could not denormalize object of type %s, no supporting normalizer found.', $class));
    }

    /**
     * {@inheritdoc}
     */
    public function supportsEncoding($format)
    {
        return $this->encoder->supportsEncoding($format);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsDecoding($format)
    {
        return $this->decoder->supportsDecoding($format);
    }
}


text:
<!--L
  Copyright Northrop Grumman Information Technology.

  Distributed under the OSI-approved BSD 3-Clause License.
  See http://ncip.github.com/nci-mapping-tool/LICENSE.txt for details.
L-->

<ehcache>
<diskStore path="@ehcache.cache.path@"/>

<cacheManagerEventListenerFactory class="" properties=""/>

<defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="1800"
        timeToLiveSeconds="7200"
        overflowToDisk="true"
        />

<cache name="treeCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="7200"
overflowToDisk="true"
/>

</ehcache>


text:
require "spec_helper"

describe Docks::Tags::Base do
  subject { Docks::Tags::Base.instance }

  it "has no synonyms by default" do
    expect(subject.synonyms).to eq []
  end

  it "is multiline by default" do
    expect(subject.multiline?).to be true
  end

  it "allows only one tag per block by default" do
    expect(subject.multiple_allowed?).to be false
  end

  it "can be included in parse results" do
    expect(subject.parseable?).to be true
  end
end


text:
import markdown from 'markdown-in-js'
import withDoc, { components } from '~/lib/with-doc'

import { expoteam } from '~/data/team'
// import { InternalLink, ExternalLink } from "~/components/text/link";
// import { P } from "~/components/text/paragraph";
// import Image from '~/components/base/image'
import { Code, InlineCode } from '~/components/base/code'
// import {
//   TerminalInput,
//   TerminalOutput
// } from "~/components/text/terminal";

// prettier-ignore
export default withDoc({
  title: 'Google',

  authors: [expoteam],
})(markdown(components)`

Provides Google authentication integration for Expo apps, using either the native Google Sign In SDK (only in standalone apps) or a system web browser (not WebView, so credentials saved on the device can be re-used!).

You'll get an access token after a succesful login. Once you have the token, if you would like to make further calls to the Google API, you can use Google's [REST APIs](https://developers.google.com/apis-explorer/) directly through HTTP (using [fetch](https://facebook.github.io/react-native/docs/network.html#fetch), for example).

${
  {`// Example of using the Google REST API
  async function getUserInfo(accessToken) {
    let userInfoResponse = await fetch('https://www.googleapis.com/userinfo/v2/me', {
      headers: { Authorization: \`Bearer \${accessToken}\`},
    });

    return userInfoResponse;
  }`}}

## Usage

### Expo.Google.logInAsync(options)

Prompts the user to log into Google and grants your app permission to access some of their Google data, as specified by the scopes.

#### :param object options

A map of options:

-   **behavior** (_string_) -- The type of behavior to use for login, either \`web\` or \`system\`. Native (\`system\`) can only be used inside of a standalone app when built using the steps described below. Default is \`web\` inside of Expo app, and \`system\` in standalone. The only case where you would need to change this is if you would prefer to use \`web\` inside of a standalone app.
-   **scopes** (_array_) -- An array specifying the scopes to ask for from Google for this login ([more information here](https://gsuite-developers.googleblog.com/2012/01/tips-on-using-apis-discovery-service.html)). Default scopes are \`['profile', 'email']\`.
-   **androidClientId** (_string_) -- The Android client id registered with Google for use in the Expo client app.
-   **iosClientId** (_string_) -- The iOS client id registered with Google for use in the Expo client app.
-   **androidStandaloneAppClientId** (_string_) -- The Android client id registered with Google for use in a standalone app.
-   **iosStandaloneAppClientId** (_string_) -- The iOS client id registered with Google for use in a standalone app.

#### Returns

If the user or Google cancelled the login, returns ${<InlineCode>{`{ type: 'cancel' }`}</InlineCode>}.

Otherwise, returns ${<InlineCode>{`{ type: 'success', accessToken, idToken, refreshToken, {...profileInformation} }`}</InlineCode>}, \`accessToken\` is a string giving the access token to use with Google HTTP API requests.

## Using it inside of the Expo app

In the Expo client app, you can only use browser-based login (this works very well actually because it re-uses credentials saved in your system browser). If you build a standalone app, you can use the native login for the platform.

To use Google Sign In, you will need to create a project on the Google Developer Console and create an OAuth 2.0 client ID. This is, unfortunately, super annoying to do and we wish there was a way we could automate this for you, but at the moment the Google Developer Console does not expose an API. _You also need to register a separate set of Client IDs for a standalone app, the process for this is described later in this document_.

-   **Get an app set up on the Google Developer Console**

   -   Go to the [Credentials Page](https://console.developers.google.com/apis/credentials)
   -   Create an app for your project if you haven't already.
   -   Once that's done, click "Create Credentials" and then "OAuth client ID." You will be prompted to set the product name on the consent screen, go ahead and do that.

-   **Create an iOS OAuth Client ID**

   -   Select "iOS Application" as the Application Type. Give it a name if you want (e.g. "iOS Development").
   -   Use \`host.exp.exponent\` as the bundle identifier.
   -   Click "Create"
   -   You will now see a modal with the client ID.
   -   The client ID is used in the \`iosClientId\` option for \`Expo.Google.loginAsync\` (see code example below).

-   **Create an Android OAuth Client ID**

   -   Select "Android Application" as the Application Type. Give it a name if you want (maybe "Android Development").
   -   Run \`openssl rand -base64 32 | openssl sha1 -c\` in your terminal, it will output a string that looks like \`A1:B2:C3\` but longer. Copy the output to your clipboard.
   -   Paste the output from the previous step into the "Signing-certificate fingerprint" text field.
   -   Use \`host.exp.exponent\` as the "Package name".
   -   Click "Create"
   -   You will now see a modal with the Client ID.
   -   The client ID is used in the \`androidClientId\` option for \`Expo.Google.loginAsync\` (see code example below).

-   **Add the Client IDs to your app**

  ${
    {`import Expo from 'expo';

       async function signInWithGoogleAsync() {
         try {
           const result = await Expo.Google.logInAsync({
             androidClientId: YOUR_CLIENT_ID_HERE,
             iosClientId: YOUR_CLIENT_ID_HERE,
             scopes: ['profile', 'email'],
           });

           if (result.type === 'success') {
             return result.accessToken;
           } else {
             return {cancelled: true};
           }
         } catch(e) {
           return {error: true};
         }
       }`}
    }

## Deploying to a standalone app on Android

If you want to use Google Sign In for a standalone app, you can follow these steps. These steps assume that you already have it working on the Expo client app. If you have already created an API key for Google Maps, you skip steps 3 through 8, inclusive.

-   **Get a Google API Key for your app** (_skip this if you already have one, eg: for Google Maps_)
   1.  Build a standalone app and download the apk, or find one that you have already built.
   2.  Go to the [Google Developer Credentials](https://console.developers.google.com/apis/credentials)
   3.  Click **Create credentials**, then **API Key**, and finally click **RESTRICT KEY** in the modal that pops up.
   4.  Click the **Android apps** radio button under **Key restriction**, then click **+ Add package name and fingerprint**.
   5.  Add your \`android.package\` from \`app.json\` (eg: \`ca.brentvatne.growlerprowler\`) to the **Package name** field.
   6.  Run ${<InlineCode>{`keytool -list -printcert -jarfile growler.apk | grep SHA1 | awk '{ print $2 }'`}</InlineCode>} (where \`growler.apk\` is the name of the apk produced in step 1).
   7.  Take the output from the previous step and insert it in the **SHA-1 certificate fingerprint** field.
   8.  Press **Save**.
-   **Get an OAuth client ID for your app**
   1.  Build a standalone app and download the apk, or find one that you have already built.
   2.  Go to the [Google Developer Credentials](https://console.developers.google.com/apis/credentials).
   3.  Click **Create credentials**, then **OAuth client ID**, then select the **Android** radio button.
   4.  Run ${<InlineCode>{`keytool -list -printcert -jarfile growler.apk | grep SHA1 | awk '{ print $2 }'`}</InlineCode>} (where \`growler.apk\` is the name of the apk produced in step 1).
   5.  Take the output from the previous step and insert it in the **Signing-certificate fingerprint** field.
   6.  Add your \`android.package\` from \`app.json\` (eg: \`ca.brentvatne.growlerprowler\`) to the **Package name** field.
   7.  Press **Create**.
-   **Add the configuration to your app**
   1.  Build a standalone app and download the apk, or find one that you have already built.
   2.  Go to the [Google Developer Credentials](https://console.developers.google.com/apis/credentials) and find your API key.
   3.  Open \`app.json\` and add your **Google API Key** to \`android.config.googleSignIn.apiKey\`.
   4.  Run ${<InlineCode>{`keytool -list -printcert -jarfile growler.apk | grep SHA1 | awk '{ print $2 }' | sed -e 's/\://g'`}</InlineCode>} (where \`growler.apk\` is the name of the apk produced in step 1).
   5.  Add the result from the previous step to \`app.json\` under \`android.config.googleSignIn.certificateHash\`.
   6.  When you use \`Expo.Google.logInAsync(..)\`, pass in the **OAuth client ID** as the \`androidStandaloneAppClientId\` option.
   7.  Rebuild your standalone app.

## Deploying to a standalone app on iOS

If you want to use native sign in for a standalone app, you can follow these steps. These steps assume that you already have it working on the Expo client app.

1.  Add a \`bundleIdentifier\` to your \`app.json\` if you don't already have one.
2.  Open your browser to [Google Developer Credentials](https://console.developers.google.com/apis/credentials)
3.  Click **Create credentials** and then **OAuth client ID**, then choose **iOS**.
4.  Provide your \`bundleIdentifier\` in the **Bundle ID** field, then press **Create**.
5.  Add the given **iOS URL scheme** to your \`app.json\` under \`ios.config.googleSignIn.reservedClientId\`.
6.  Wherever you use \`Expo.Google.logInAsync\`, provide the **OAuth client ID** as the \`iosStandaloneAppClientId\` option.
7.  Rebuild your standalone app.

`)