VLCGoogleDriveController.m 13.8 KB
Newer Older
1 2 3 4 5 6 7 8 9
/*****************************************************************************
 * VLCGoogleDriveController.m
 * VLC for iOS
 *****************************************************************************
 * Copyright (c) 2013 VideoLAN. All rights reserved.
 * $Id$
 *
 * Authors: Carola Nitz <nitz.carola # googlemail.com>
 *          Felix Paul Kühne <fkuehne # videolan.org>
10
 *          Soomin Lee <TheHungryBu # gmail.com>
11 12 13
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
Carola Nitz's avatar
Carola Nitz committed
14 15 16

#import "VLCGoogleDriveController.h"
#import "NSString+SupportedMedia.h"
17
#import "VLCPlaybackController.h"
18
#import "VLCMediaFileDiscoverer.h"
19
#import "VLC-Swift.h"
20
#import <XKKeychain/XKKeychain.h>
Carola Nitz's avatar
Carola Nitz committed
21

22 23
#import <AppAuth/AppAuth.h>
#import <GTMAppAuth/GTMAppAuth.h>
24

Carola Nitz's avatar
Carola Nitz committed
25 26 27 28
@interface VLCGoogleDriveController ()
{
    GTLDriveFileList *_fileList;
    GTLServiceTicket *_fileListTicket;
29

Carola Nitz's avatar
Carola Nitz committed
30 31 32 33 34
    NSArray *_currentFileList;

    NSMutableArray *_listOfGoogleDriveFilesToDownload;
    BOOL _downloadInProgress;

35
    NSString *_nextPageToken;
36
    NSString *_folderId;
37 38 39 40

    CGFloat _averageSpeed;
    NSTimeInterval _startDL;
    NSTimeInterval _lastStatsUpdate;
Carola Nitz's avatar
Carola Nitz committed
41 42 43 44 45 46 47 48
}

@end

@implementation VLCGoogleDriveController

#pragma mark - session handling

Tobias's avatar
Tobias committed
49
+ (instancetype)sharedInstance
50
{
51 52
    static VLCGoogleDriveController *sharedInstance = nil;
    static dispatch_once_t pred;
53

54
    dispatch_once(&pred, ^{
55
        sharedInstance = [VLCGoogleDriveController new];
56
    });
57

58
    return sharedInstance;
59 60
}

Carola Nitz's avatar
Carola Nitz committed
61 62
- (void)startSession
{
63
    [self restoreFromSharedCredentials];
64
    self.driveService = [GTLServiceDrive new];
65
    self.driveService.authorizer = [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kKeychainItemName];
Carola Nitz's avatar
Carola Nitz committed
66 67
}

68 69 70 71 72 73 74
- (void)stopSession
{
    [_fileListTicket cancelTicket];
    _nextPageToken = nil;
    _currentFileList = nil;
}

Carola Nitz's avatar
Carola Nitz committed
75 76
- (void)logout
{
77
    self.driveService.authorizer = nil;
78 79 80
    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
    [ubiquitousStore setString:nil forKey:kVLCStoreGDriveCredentials];
    [ubiquitousStore synchronize];
81
    [self stopSession];
82
    if ([self.delegate respondsToSelector:@selector(mediaListUpdated)])
Carola Nitz's avatar
Carola Nitz committed
83
        [self.delegate mediaListUpdated];
Carola Nitz's avatar
Carola Nitz committed
84 85 86 87
}

- (BOOL)isAuthorized
{
88 89 90
    if (!self.driveService) {
        [self startSession];
    }
91 92 93

    BOOL ret = [(GTMAppAuthFetcherAuthorization *)self.driveService.authorizer canAuthorize];

94 95 96 97 98 99 100 101 102
    if (ret) {
        [self shareCredentials];
    }
    return ret;
}

- (void)shareCredentials
{
    /* share our credentials */
103 104
    XKKeychainGenericPasswordItem *item = [XKKeychainGenericPasswordItem itemForService:kKeychainItemName account:@"OAuth" error:nil]; // kGTMOAuth2AccountName
    NSString *credentials = item.secret.stringValue;
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    if (credentials == nil)
        return;

    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
    [ubiquitousStore setString:credentials forKey:kVLCStoreGDriveCredentials];
    [ubiquitousStore synchronize];
}

- (BOOL)restoreFromSharedCredentials
{
    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
    [ubiquitousStore synchronize];
    NSString *credentials = [ubiquitousStore stringForKey:kVLCStoreGDriveCredentials];
    if (!credentials)
        return NO;

121 122 123 124 125 126
    XKKeychainGenericPasswordItem *keychainItem = [[XKKeychainGenericPasswordItem alloc] init];
    keychainItem.service = kKeychainItemName;
    keychainItem.account = @"OAuth"; // kGTMOAuth2AccountName
    keychainItem.secret.stringValue = credentials;
    [keychainItem saveWithError:nil];

127
    return YES;
Carola Nitz's avatar
Carola Nitz committed
128 129 130 131
}

- (void)showAlert:(NSString *)title message:(NSString *)message
{
132 133
    [VLCAlertViewController alertViewManagerWithTitle:title
                                         errorMessage:message
134
                                       viewController:[UIApplication sharedApplication].keyWindow.rootViewController];
Carola Nitz's avatar
Carola Nitz committed
135 136 137
}

#pragma mark - file management
138 139 140 141 142 143

- (BOOL)canPlayAll
{
    return NO;
}

144
- (void)requestDirectoryListingAtPath:(NSString *)path
Carola Nitz's avatar
Carola Nitz committed
145
{
146 147
    if (self.isAuthorized) {
        //we entered a different folder so discard all current files
148
        if (![path isEqualToString:_folderId])
149
            _currentFileList = nil;
150
        [self listFilesWithID:path];
151
    }
Carola Nitz's avatar
Carola Nitz committed
152 153
}

154 155 156 157 158
- (BOOL)hasMoreFiles
{
    return _nextPageToken != nil;
}

159
- (void)downloadFileToDocumentFolder:(GTLDriveFile *)file
Carola Nitz's avatar
Carola Nitz committed
160
{
161 162 163
    if (file == nil)
        return;

164 165
    if ([file.mimeType isEqualToString:@"application/vnd.google-apps.folder"]) return;

166 167
    if (!_listOfGoogleDriveFilesToDownload)
        _listOfGoogleDriveFilesToDownload = [[NSMutableArray alloc] init];
Carola Nitz's avatar
Carola Nitz committed
168

169
    [_listOfGoogleDriveFilesToDownload addObject:file];
Carola Nitz's avatar
Carola Nitz committed
170

171 172 173 174
    if ([self.delegate respondsToSelector:@selector(numberOfFilesWaitingToBeDownloadedChanged)])
        [self.delegate numberOfFilesWaitingToBeDownloadedChanged];

    [self _triggerNextDownload];
Carola Nitz's avatar
Carola Nitz committed
175 176
}

177
- (void)listFilesWithID:(NSString *)folderId
Carola Nitz's avatar
Carola Nitz committed
178 179
{
    _fileList = nil;
180
    _folderId = folderId;
181
    GTLQueryDrive *query;
182
    NSString *parentName = @"root";
Carola Nitz's avatar
Carola Nitz committed
183

184 185
    query = [GTLQueryDrive queryForFilesList];
    query.pageToken = _nextPageToken;
186 187 188 189 190
    //the results don't come in alphabetical order when paging. So the maxresult (default 100) is set to 1000 in order to get a few more files at once.
    //query.pageSize = 1000;
    query.includeDeleted = NO;
    query.includeRemoved = NO;
    query.restrictToMyDrive = YES;
191 192
    query.fields = @"files(*)";

193
    if (![_folderId isEqualToString:@""]) {
194
        parentName = [_folderId lastPathComponent];
195
    }
196 197
    query.q = [NSString stringWithFormat:@"'%@' in parents", parentName];

198
    _fileListTicket = [self.driveService executeQuery:query
Carola Nitz's avatar
Carola Nitz committed
199 200 201
                          completionHandler:^(GTLServiceTicket *ticket,
                                              GTLDriveFileList *fileList,
                                              NSError *error) {
202
                              if (error == nil) {
203 204 205
                                  self->_fileList = fileList;
                                  self->_nextPageToken = fileList.nextPageToken;
                                  self->_fileListTicket = nil;
206
                                  [self _listOfGoodFilesAndFolders];
207
                              } else {
208
                                  [self showAlert:NSLocalizedString(@"GDRIVE_ERROR_FETCHING_FILES",nil) message:error.localizedDescription];
209
                              }
Carola Nitz's avatar
Carola Nitz committed
210 211 212
                          }];
}

Carola Nitz's avatar
Carola Nitz committed
213 214
- (void)streamFile:(GTLDriveFile *)file
{
215
    NSString *token = [((GTMAppAuthFetcherAuthorization *)self.driveService.authorizer).authState.lastTokenResponse accessToken];
216 217 218
    NSString *urlString = [NSString stringWithFormat:@"https://www.googleapis.com/drive/v3/files/%@?alt=media&access_token=%@",
                     file.identifier, token];

219
    VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance];
220 221 222 223
    VLCMedia *media = [VLCMedia mediaWithURL:[NSURL URLWithString:urlString]];
    VLCMediaList *medialist = [[VLCMediaList alloc] init];
    [medialist addMedia:media];
    [vpc playMediaList:medialist firstIndex:0 subtitlesFilePath:nil];
Carola Nitz's avatar
Carola Nitz committed
224 225
}

Carola Nitz's avatar
Carola Nitz committed
226 227 228 229 230 231 232 233 234 235 236
- (void)_triggerNextDownload
{
    if (_listOfGoogleDriveFilesToDownload.count > 0 && !_downloadInProgress) {
        [self _reallyDownloadFileToDocumentFolder:_listOfGoogleDriveFilesToDownload[0]];
        [_listOfGoogleDriveFilesToDownload removeObjectAtIndex:0];

        if ([self.delegate respondsToSelector:@selector(numberOfFilesWaitingToBeDownloadedChanged)])
            [self.delegate numberOfFilesWaitingToBeDownloadedChanged];
    }
}

237
- (void)_reallyDownloadFileToDocumentFolder:(GTLDriveFile *)file
Carola Nitz's avatar
Carola Nitz committed
238 239
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
240
    NSString *filePath = [searchPaths[0] stringByAppendingFormat:@"/%@", file.originalFilename];
Carola Nitz's avatar
Carola Nitz committed
241

242
    [self loadFile:file intoPath:filePath];
Carola Nitz's avatar
Carola Nitz committed
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257

    if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStarted)])
        [self.delegate operationWithProgressInformationStarted];

    _downloadInProgress = YES;
}

- (BOOL)_supportedFileExtension:(NSString *)filename
{
    if ([filename isSupportedMediaFormat] || [filename isSupportedAudioMediaFormat] || [filename isSupportedSubtitleFormat])
        return YES;

    return NO;
}

258
- (void)_listOfGoodFilesAndFolders
Carola Nitz's avatar
Carola Nitz committed
259 260
{
    NSMutableArray *listOfGoodFilesAndFolders = [[NSMutableArray alloc] init];
261

262
    for (GTLDriveFile *iter in _fileList.files) {
263 264 265 266
        if (iter.trashed.boolValue) {
            continue;
        }

267 268
        BOOL isDirectory = [iter.mimeType isEqualToString:@"application/vnd.google-apps.folder"];
        BOOL supportedFile = [self _supportedFileExtension:iter.name];
269

270
        if (isDirectory || supportedFile)
271
            [listOfGoodFilesAndFolders addObject:iter];
Carola Nitz's avatar
Carola Nitz committed
272
    }
273
    _currentFileList = [NSArray arrayWithArray:listOfGoodFilesAndFolders];
Carola Nitz's avatar
Carola Nitz committed
274

275
    if ([_currentFileList count] <= 10 && [self hasMoreFiles]) {
276
        [self listFilesWithID:_folderId];
277 278
        return;
    }
Carola Nitz's avatar
Carola Nitz committed
279

280
    APLog(@"found filtered metadata for %lu files", (unsigned long)_currentFileList.count);
281 282 283

    //the files come in a chaotic order so we order alphabetically
     NSArray *sortedArray = [_currentFileList sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
284 285
        NSString *first = [(GTLDriveFile *)a name];
        NSString *second = [(GTLDriveFile *)b name];
286 287 288 289
        return [first compare:second];
    }];
    _currentFileList = sortedArray;

Carola Nitz's avatar
Carola Nitz committed
290 291 292 293
    if ([self.delegate respondsToSelector:@selector(mediaListUpdated)])
        [self.delegate mediaListUpdated];
}

294 295
- (void)loadFile:(GTLDriveFile*)file intoPath:(NSString*)destinationPath
{
296 297
    NSString *exportURLStr =  [NSString stringWithFormat:@"https://www.googleapis.com/drive/v3/files/%@?alt=media",
                           file.identifier];
298 299

    if ([exportURLStr length] > 0) {
300
        GTMSessionFetcher *fetcher = [self.driveService.fetcherService fetcherWithURLString:exportURLStr];
301
        fetcher.authorizer = self.driveService.authorizer;
302 303

        fetcher.destinationFileURL = [NSURL fileURLWithPath:destinationPath isDirectory:YES];
304 305

        // Fetcher logging can include comments.
306 307
        [fetcher setCommentWithFormat:@"Downloading \"%@\"", file.name];
        __weak GTMSessionFetcher *weakFetcher = fetcher;
308
        _startDL = [NSDate timeIntervalSinceReferenceDate];
309 310 311
        fetcher.downloadProgressBlock = ^(int64_t bytesWritten,
                                          int64_t totalBytesWritten,
                                          int64_t totalBytesExpectedToWrite) {
312
            if ((self->_lastStatsUpdate > 0 && ([NSDate timeIntervalSinceReferenceDate] - self->_lastStatsUpdate > .5)) || self->_lastStatsUpdate <= 0) {
313
                [self calculateRemainingTime:totalBytesWritten expectedDownloadSize:totalBytesExpectedToWrite];
314
                self->_lastStatsUpdate = [NSDate timeIntervalSinceReferenceDate];
315
            }
316

317
            CGFloat progress = (CGFloat)weakFetcher.downloadedLength / (CGFloat)[file.size unsignedLongValue];
318 319 320 321 322 323
            if ([self.delegate respondsToSelector:@selector(currentProgressInformation:)])
                [self.delegate currentProgressInformation:progress];
        };

        [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
            if (error == nil) {
Carola Nitz's avatar
Carola Nitz committed
324 325
                //TODO: show something nice than an annoying alert
                //[self showAlert:NSLocalizedString(@"GDRIVE_DOWNLOAD_SUCCESSFUL_TITLE",nil) message:NSLocalizedString(@"GDRIVE_DOWNLOAD_SUCCESSFUL",nil)];
326
                [self downloadSuccessful];
327
            } else {
328
                [self showAlert:NSLocalizedString(@"GDRIVE_ERROR_DOWNLOADING_FILE_TITLE",nil) message:NSLocalizedString(@"GDRIVE_ERROR_DOWNLOADING_FILE",nil)];
329 330 331
                [self downloadFailedWithError:error];
            }
        }];
Carola Nitz's avatar
Carola Nitz committed
332 333 334
    }
}

335 336 337 338 339 340 341 342 343 344 345 346 347
- (void)calculateRemainingTime:(CGFloat)receivedDataSize expectedDownloadSize:(CGFloat)expectedDownloadSize
{
    CGFloat lastSpeed = receivedDataSize / ([NSDate timeIntervalSinceReferenceDate] - _startDL);
    CGFloat smoothingFactor = 0.005;
    _averageSpeed = isnan(_averageSpeed) ? lastSpeed : smoothingFactor * lastSpeed + (1 - smoothingFactor) * _averageSpeed;

    CGFloat RemainingInSeconds = (expectedDownloadSize - receivedDataSize) / _averageSpeed;

    NSDate *date = [NSDate dateWithTimeIntervalSince1970:RemainingInSeconds];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"HH:mm:ss"];
    [formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];

348
    NSString  *remainingTime = [formatter stringFromDate:date];
349
    if ([self.delegate respondsToSelector:@selector(updateRemainingTime:)])
350
        [self.delegate updateRemainingTime:remainingTime];
351 352
}

353
- (void)downloadSuccessful
354 355
{
    /* update library now that we got a file */
356
    APLog(@"DriveFile download was successful");
357
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"GDRIVE_DOWNLOAD_SUCCESSFUL", nil));
358
    [[VLCMediaFileDiscoverer sharedInstance] performSelectorOnMainThread:@selector(updateMediaList) withObject:nil waitUntilDone:NO];
359 360 361 362 363 364 365 366 367 368

    if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStopped)])
        [self.delegate operationWithProgressInformationStopped];
    _downloadInProgress = NO;

    [self _triggerNextDownload];
}

- (void)downloadFailedWithError:(NSError*)error
{
369
    APLog(@"DriveFile download failed with error %li", (long)error.code);
370 371 372 373 374 375 376
    if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStopped)])
        [self.delegate operationWithProgressInformationStopped];
    _downloadInProgress = NO;

    [self _triggerNextDownload];
}

Carola Nitz's avatar
Carola Nitz committed
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
#pragma mark - VLC internal communication and delegate

- (NSArray *)currentListFiles
{
    return _currentFileList;
}

- (NSInteger)numberOfFilesWaitingToBeDownloaded
{
    if (_listOfGoogleDriveFilesToDownload)
        return _listOfGoogleDriveFilesToDownload.count;

    return 0;
}

@end