VLCDropboxController.m 13.6 KB
Newer Older
1 2 3 4
/*****************************************************************************
 * VLCDropboxController.m
 * VLC for iOS
 *****************************************************************************
5
 * Copyright (c) 2013-2015 VideoLAN. All rights reserved.
6 7 8 9 10 11 12
 * $Id$
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan.org>
 *          Jean-Baptiste Kempf <jb # videolan.org>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
13 14

#import "VLCDropboxController.h"
15
#import "NSString+SupportedMedia.h"
16
#import "VLCPlaybackController.h"
17 18
#import "VLCActivityManager.h"
#import "VLCMediaFileDiscoverer.h"
19
#import "VLCDropboxConstants.h"
20 21 22 23

#if TARGET_OS_IOS
# import "VLC-Swift.h"
#endif
24 25 26

@interface VLCDropboxController ()

27 28
@property (strong, nonatomic) DBUserClient *client;
@property (strong, nonatomic) NSArray *currentFileList;
29

30 31
@property (strong, nonatomic) NSMutableArray *listOfDropboxFilesToDownload;
@property (assign, nonatomic) BOOL downloadInProgress;
32

33 34 35 36 37
@property (assign, nonatomic) CGFloat averageSpeed;
@property (assign, nonatomic) NSTimeInterval startDL;
@property (assign, nonatomic) NSTimeInterval lastStatsUpdate;

@property (strong, nonatomic) UINavigationController *lastKnownNavigationController;
38 39 40 41 42 43 44

@end

@implementation VLCDropboxController

#pragma mark - session handling

45 46 47 48 49 50
+ (instancetype)sharedInstance
{
    static VLCDropboxController *sharedInstance = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred, ^{
51
        sharedInstance = [VLCDropboxController new];
52
        [sharedInstance shareCredentials];
53 54 55 56 57
    });

    return sharedInstance;
}

58 59 60
- (void)shareCredentials
{
    /* share our credentials */
61
    NSArray *credentials = [DBSDKKeychain retrieveAllTokenIds];
62 63 64 65
    if (credentials == nil)
        return;

    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
66
    [ubiquitousStore setArray:credentials forKey:kVLCStoreDropboxCredentials];
67 68 69 70 71 72 73
    [ubiquitousStore synchronize];
}

- (BOOL)restoreFromSharedCredentials
{
    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
    [ubiquitousStore synchronize];
74
    NSArray *credentials = [ubiquitousStore arrayForKey:kVLCStoreDropboxCredentials];
75
    if (!credentials) {
76
        return NO;
77
    }
78 79 80
    for (NSString *tmp in credentials) {
        [DBSDKKeychain storeValueWithKey:kVLCStoreDropboxCredentials value:tmp];
    }
81 82 83
    return YES;
}

84 85
- (void)startSession
{
86
    [DBClientsManager authorizedClient];
87 88 89 90
}

- (void)logout
{
91
    [DBClientsManager unlinkAndResetClients];
92 93
}

94
- (BOOL)isAuthorized
95
{
96
    return [DBClientsManager authorizedClient];
97 98
}

99 100 101
- (DBUserClient *)client {
    if (!_client) {
        _client = [DBClientsManager authorizedClient];
102
    }
103
    return _client;
104 105
}

106

107
#pragma mark - file management
108

109 110
- (BOOL)_supportedFileExtension:(NSString *)filename
{
Carola Nitz's avatar
Carola Nitz committed
111 112 113
    return [filename isSupportedMediaFormat]
        || [filename isSupportedAudioMediaFormat]
        || [filename isSupportedSubtitleFormat];
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
}

- (NSString *)_createPotentialNameFrom:(NSString *)path
{
    NSFileManager *fileManager = [NSFileManager defaultManager];

    NSString *fileName = [path lastPathComponent];
    NSString *finalFilePath = [path stringByDeletingLastPathComponent];

    if ([fileManager fileExistsAtPath:path]) {
        NSString *potentialFilename;
        NSString *fileExtension = [fileName pathExtension];
        NSString *rawFileName = [fileName stringByDeletingPathExtension];
        for (NSUInteger x = 1; x < 100; x++) {
            potentialFilename = [NSString stringWithFormat:@"%@_%lu.%@", rawFileName, (unsigned long)x, fileExtension];
129
            if (![fileManager fileExistsAtPath:[finalFilePath stringByAppendingPathComponent:potentialFilename]]) {
130
                break;
131
            }
132 133 134 135 136 137
        }
        return [finalFilePath stringByAppendingPathComponent:potentialFilename];
    }
    return path;
}

138 139 140 141 142
- (BOOL)canPlayAll
{
    return NO;
}

143 144
- (void)requestDirectoryListingAtPath:(NSString *)path
{
145
    if (self.isAuthorized) {
146
        [self listFiles:path];
147
    }
148 149
}

150
- (void)downloadFileToDocumentFolder:(DBFILESMetadata *)file
151
{
152
    if (![file isKindOfClass:[DBFILESFolderMetadata class]]) {
153 154 155 156
        if (!self.listOfDropboxFilesToDownload) {
            self.listOfDropboxFilesToDownload = [[NSMutableArray alloc] init];
        }
        [self.listOfDropboxFilesToDownload addObject:file];
157

158
        if ([self.delegate respondsToSelector:@selector(numberOfFilesWaitingToBeDownloadedChanged)]) {
159
            [self.delegate numberOfFilesWaitingToBeDownloadedChanged];
160
        }
161

162
        [self _triggerNextDownload];
163 164 165
    }
}

166 167
- (void)_triggerNextDownload
{
168 169 170
    if (self.listOfDropboxFilesToDownload.count > 0 && !self.downloadInProgress) {
        [self _reallyDownloadFileToDocumentFolder:self.listOfDropboxFilesToDownload[0]];
        [self.listOfDropboxFilesToDownload removeObjectAtIndex:0];
171

172
        if ([self.delegate respondsToSelector:@selector(numberOfFilesWaitingToBeDownloadedChanged)]) {
173
            [self.delegate numberOfFilesWaitingToBeDownloadedChanged];
174
        }
175 176 177
    }
}

178
- (void)_reallyDownloadFileToDocumentFolder:(DBFILESFileMetadata *)file
179 180
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
181
    NSString *filePath = [searchPaths[0] stringByAppendingFormat:@"/%@", file.name];
182
    self.startDL = [NSDate timeIntervalSinceReferenceDate];
183 184

    [self downloadFileFrom:file.pathDisplay to:filePath];
185

186
    if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStarted)]) {
187
        [self.delegate operationWithProgressInformationStarted];
188
    }
189

190
    self.downloadInProgress = YES;
191 192
}

193 194 195 196
- (void)streamFile:(DBFILESMetadata *)file currentNavigationController:(UINavigationController *)navigationController
{
    if (![file isKindOfClass:[DBFILESFolderMetadata class]]) {
        _lastKnownNavigationController = navigationController;
Carola Nitz's avatar
Carola Nitz committed
197
        [self loadStreamFrom:file.pathLower];
198
    }
199 200
}

201
# pragma mark - Dropbox API Request
202

203
- (void)listFiles:(NSString *)path
204
{
205
    // DropBox API prefers an empty path than a '/'
206
    if (!path || [path isEqualToString:@"/"]) {
207 208 209 210
        path = @"";
    }
    [[[self client].filesRoutes listFolder:path] setResponseBlock:^(DBFILESListFolderResult * _Nullable result, DBFILESListFolderError * _Nullable routeError, DBRequestError * _Nullable networkError) {
        if (result) {
211
            self.currentFileList = [result.entries sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
212 213 214 215
                NSString *first = [(DBFILESMetadata*)a name];
                NSString *second = [(DBFILESMetadata*)b name];
                return [first caseInsensitiveCompare:second];
            }];
216
            APLog(@"found filtered metadata for %lu files", (unsigned long)self.currentFileList.count);
217 218 219 220 221 222 223
            if ([self.delegate respondsToSelector:@selector(mediaListUpdated)])
                [self.delegate mediaListUpdated];
        } else {
            APLog(@"listFiles failed with network error %li and error tag %li", (long)networkError.statusCode, (long)networkError.tag);
            [self _handleError:[NSError errorWithDomain:networkError.description code:networkError.statusCode.integerValue userInfo:nil]];
        }
    }];
224 225
}

226
- (void)downloadFileFrom:(NSString *)path to:(NSString *)destination
227
{
228 229 230 231
    if (![self _supportedFileExtension:[path lastPathComponent]]) {
        [self _handleError:[NSError errorWithDomain:NSLocalizedString(@"FILE_NOT_SUPPORTED", nil) code:415 userInfo:nil]];
        return;
    }
232

233 234 235 236 237
    if (!destination) {
        [self _handleError:[NSError errorWithDomain:NSLocalizedString(@"GDRIVE_ERROR_DOWNLOADING_FILE", nil) code:415 userInfo:nil]];
        return;
    }

238
    // Need to replace all ' ' by '_' because it causes a `NSInvalidArgumentException ... destination path is nil` in the dropbox library.
Carola Nitz's avatar
Carola Nitz committed
239
    destination = [destination stringByReplacingOccurrencesOfString:@" " withString:@"_"];
240

241
    destination = [self _createPotentialNameFrom:destination];
242

243
    [[[self.client.filesRoutes downloadUrl:path overwrite:YES destination:[NSURL URLWithString:destination]]
244
        setResponseBlock:^(DBFILESFileMetadata * _Nullable result, DBFILESDownloadError * _Nullable routeError, DBRequestError * _Nullable networkError, NSURL * _Nonnull destination) {
245

246 247 248
            if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStopped)]) {
                [self.delegate operationWithProgressInformationStopped];
            }
249

250
#if TARGET_OS_IOS
251 252 253
            // FIXME: Replace notifications by cleaner observers
            [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.VLCNewFileAddedNotification
                                                                object:self];
254
#endif
255
            self.downloadInProgress = NO;
256
            [self _triggerNextDownload];
257
            if (networkError) {
258
                APLog(@"downloadFile failed with network error %li and error tag %li", (long)networkError.statusCode, (long)networkError.tag);
259
                [self _handleError:networkError.nsError];
260 261
            }
        }] setProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
262 263 264 265
            if (totalBytesWritten == totalBytesExpectedToWrite) {
                UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"GDRIVE_DOWNLOAD_SUCCESSFUL", nil));
            }

266
            if ((self.lastStatsUpdate > 0 && ([NSDate timeIntervalSinceReferenceDate] - self.lastStatsUpdate > .5)) || self.lastStatsUpdate <= 0) {
267
                [self calculateRemainingTime:(CGFloat)totalBytesWritten expectedDownloadSize:(CGFloat)totalBytesExpectedToWrite];
268
                self.lastStatsUpdate = [NSDate timeIntervalSinceReferenceDate];
269
            }
270

271 272 273
            if ([self.delegate respondsToSelector:@selector(currentProgressInformation:)])
                [self.delegate currentProgressInformation:(CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite];
        }];
274 275 276

}

277
- (void)loadStreamFrom:(NSString *)path
278
{
Carola Nitz's avatar
Carola Nitz committed
279
    if (!path || ![self _supportedFileExtension:[path lastPathComponent]]) {
280 281
        [self _handleError:[NSError errorWithDomain:NSLocalizedString(@"FILE_NOT_SUPPORTED", nil) code:415 userInfo:nil]];
        return;
282
    }
283

284
    [[self.client.filesRoutes getTemporaryLink:path] setResponseBlock:^(DBFILESGetTemporaryLinkResult * _Nullable result, DBFILESGetTemporaryLinkError * _Nullable routeError, DBRequestError * _Nullable networkError) {
285 286

        if (result) {
287 288 289 290
            VLCMedia *media = [VLCMedia mediaWithURL:[NSURL URLWithString:result.link]];
            VLCMediaList *medialist = [[VLCMediaList alloc] init];
            [medialist addMedia:media];
            [[VLCPlaybackController sharedInstance] playMediaList:medialist firstIndex:0 subtitlesFilePath:nil];
Carola Nitz's avatar
Carola Nitz committed
291
#if TARGET_OS_TV
292
            if (self.lastKnownNavigationController) {
293
                VLCFullscreenMovieTVViewController *movieVC = [VLCFullscreenMovieTVViewController fullscreenMovieTVViewController];
294
                [self.lastKnownNavigationController presentViewController:movieVC
295 296 297
                                                             animated:YES
                                                           completion:nil];
            }
Carola Nitz's avatar
Carola Nitz committed
298
#endif
299 300 301 302 303
        } else {
            APLog(@"loadStream failed with network error %li and error tag %li", (long)networkError.statusCode, (long)networkError.tag);
            [self _handleError:[NSError errorWithDomain:networkError.description code:networkError.statusCode.integerValue userInfo:nil]];
        }
    }];
304 305 306 307
}

#pragma mark - VLC internal communication and delegate

308 309
- (void)calculateRemainingTime:(CGFloat)receivedDataSize expectedDownloadSize:(CGFloat)expectedDownloadSize
{
310
    CGFloat lastSpeed = receivedDataSize / ([NSDate timeIntervalSinceReferenceDate] - self.startDL);
311
    CGFloat smoothingFactor = 0.005;
312
    self.averageSpeed = isnan(self.averageSpeed) ? lastSpeed : smoothingFactor * lastSpeed + (1 - smoothingFactor) * self.averageSpeed;
313

314
    CGFloat RemainingInSeconds = (expectedDownloadSize - receivedDataSize)/self.averageSpeed;
315 316 317 318 319 320 321

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

    NSString  *remaingTime = [formatter stringFromDate:date];
322
    if ([self.delegate respondsToSelector:@selector(updateRemainingTime:)]) {
323
        [self.delegate updateRemainingTime:remaingTime];
324
    }
325 326
}

327 328
- (NSInteger)numberOfFilesWaitingToBeDownloaded
{
329 330 331
    if (self.listOfDropboxFilesToDownload) {
        return self.listOfDropboxFilesToDownload.count;
    }
332 333 334
    return 0;
}

335 336 337
#pragma mark - user feedback
- (void)_handleError:(NSError *)error
{
338 339 340 341
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"ERROR_NUMBER", nil), error.code]
                                                                   message:error.localizedDescription
                                                            preferredStyle:UIAlertControllerStyleAlert];

342
    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OK", nil)
343 344 345 346 347 348
                                                            style:UIAlertActionStyleDestructive
                                                          handler:^(UIAlertAction *action) {
                                                          }];

    [alert addAction:defaultAction];

349
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
350 351
}

352 353
- (void)reset
{
354
    self.currentFileList = nil;
355 356
}

357
@end