VLCDropboxController.m 12.8 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"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
17 18
#import "VLCActivityManager.h"
#import "VLCMediaFileDiscoverer.h"
19
#import "VLCDropboxConstants.h"
20 21 22

@interface VLCDropboxController ()
{
23
    DBUserClient *_client;
24 25
    NSArray *_currentFileList;

26 27 28
    NSMutableArray *_listOfDropboxFilesToDownload;
    BOOL _downloadInProgress;

29 30 31
    CGFloat _averageSpeed;
    NSTimeInterval _startDL;
    NSTimeInterval _lastStatsUpdate;
32 33

    UINavigationController *_lastKnownNavigationController;
34 35 36 37 38 39 40 41
}

@end

@implementation VLCDropboxController

#pragma mark - session handling

42 43 44 45 46 47
+ (instancetype)sharedInstance
{
    static VLCDropboxController *sharedInstance = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred, ^{
48
        sharedInstance = [VLCDropboxController new];
49
        [sharedInstance shareCredentials];
50 51 52 53 54
    });

    return sharedInstance;
}

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

    NSUbiquitousKeyValueStore *ubiquitousStore = [NSUbiquitousKeyValueStore defaultStore];
63
    [ubiquitousStore setArray:credentials forKey:kVLCStoreDropboxCredentials];
64 65 66 67 68 69 70
    [ubiquitousStore synchronize];
}

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

80 81
- (void)startSession
{
82
    [DBClientsManager authorizedClient];
83 84 85 86
}

- (void)logout
{
87
    [DBClientsManager unlinkAndResetClients];
88 89
}

90
- (BOOL)isAuthorized
91
{
92
    return [DBClientsManager authorizedClient];
93 94
}

95 96 97
- (DBUserClient *)client {
    if (!_client) {
        _client = [DBClientsManager authorizedClient];
98
    }
99
    return _client;
100 101
}

102

103
#pragma mark - file management
104

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

- (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];
            if (![fileManager fileExistsAtPath:[finalFilePath stringByAppendingPathComponent:potentialFilename]])
                break;
        }
        return [finalFilePath stringByAppendingPathComponent:potentialFilename];
    }
    return path;
}

133 134 135 136 137
- (BOOL)canPlayAll
{
    return NO;
}

138 139
- (void)requestDirectoryListingAtPath:(NSString *)path
{
140
    if (self.isAuthorized)
141
        [self listFiles:path];
142 143
}

144
- (void)downloadFileToDocumentFolder:(DBFILESMetadata *)file
145
{
146
    if (![file isKindOfClass:[DBFILESFolderMetadata class]]) {
147 148 149
        if (!_listOfDropboxFilesToDownload)
            _listOfDropboxFilesToDownload = [[NSMutableArray alloc] init];
        [_listOfDropboxFilesToDownload addObject:file];
150

151 152
        if ([self.delegate respondsToSelector:@selector(numberOfFilesWaitingToBeDownloadedChanged)])
            [self.delegate numberOfFilesWaitingToBeDownloadedChanged];
153

154
        [self _triggerNextDownload];
155 156 157
    }
}

158 159 160 161 162 163 164 165 166 167 168
- (void)_triggerNextDownload
{
    if (_listOfDropboxFilesToDownload.count > 0 && !_downloadInProgress) {
        [self _reallyDownloadFileToDocumentFolder:_listOfDropboxFilesToDownload[0]];
        [_listOfDropboxFilesToDownload removeObjectAtIndex:0];

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

169
- (void)_reallyDownloadFileToDocumentFolder:(DBFILESFileMetadata *)file
170 171
{
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
172
    NSString *filePath = [searchPaths[0] stringByAppendingFormat:@"/%@", file.name];
173
    _startDL = [NSDate timeIntervalSinceReferenceDate];
174 175

    [self downloadFileFrom:file.pathDisplay to:filePath];
176 177 178 179 180 181 182

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

    _downloadInProgress = YES;
}

183 184 185 186
- (void)streamFile:(DBFILESMetadata *)file currentNavigationController:(UINavigationController *)navigationController
{
    if (![file isKindOfClass:[DBFILESFolderMetadata class]]) {
        _lastKnownNavigationController = navigationController;
Carola Nitz's avatar
Carola Nitz committed
187
        [self loadStreamFrom:file.pathLower];
188
    }
189 190
}

191
# pragma mark - Dropbox API Request
192

193
- (void)listFiles:(NSString *)path
194
{
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
    // DropBox API prefers an empty path than a '/'
    if ([path isEqualToString:@"/"]) {
        path = @"";
    }
    [[[self client].filesRoutes listFolder:path] setResponseBlock:^(DBFILESListFolderResult * _Nullable result, DBFILESListFolderError * _Nullable routeError, DBRequestError * _Nullable networkError) {
        if (result) {
            _currentFileList = [NSArray arrayWithArray:result.entries];
            APLog(@"found filtered metadata for %lu files", (unsigned long)_currentFileList.count);
            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]];
        }
    }];
210 211
}

212
- (void)downloadFileFrom:(NSString *)path to:(NSString *)destination
213
{
214 215 216 217
    if (![self _supportedFileExtension:[path lastPathComponent]]) {
        [self _handleError:[NSError errorWithDomain:NSLocalizedString(@"FILE_NOT_SUPPORTED", nil) code:415 userInfo:nil]];
        return;
    }
218

219
    // 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
220
    destination = [destination stringByReplacingOccurrencesOfString:@" " withString:@"_"];
221

222
    destination = [self _createPotentialNameFrom:destination];
223

224 225
    [[[_client.filesRoutes downloadUrl:path overwrite:YES destination:[NSURL URLWithString:destination]]
        setResponseBlock:^(DBFILESFileMetadata * _Nullable result, DBFILESDownloadError * _Nullable routeError, DBRequestError * _Nullable networkError, NSURL * _Nonnull destination) {
226

227 228 229
            if ([self.delegate respondsToSelector:@selector(operationWithProgressInformationStopped)]) {
                [self.delegate operationWithProgressInformationStopped];
            }
230

231 232 233 234 235 236 237 238 239 240 241
            _downloadInProgress = NO;
            [self _triggerNextDownload];
            if (!result) {
                APLog(@"downloadFile 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]];
            }
        }] setProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
            if ((_lastStatsUpdate > 0 && ([NSDate timeIntervalSinceReferenceDate] - _lastStatsUpdate > .5)) || _lastStatsUpdate <= 0) {
                [self calculateRemainingTime:(CGFloat)totalBytesWritten expectedDownloadSize:(CGFloat)totalBytesExpectedToWrite];
                _lastStatsUpdate = [NSDate timeIntervalSinceReferenceDate];
            }
242

243 244 245
            if ([self.delegate respondsToSelector:@selector(currentProgressInformation:)])
                [self.delegate currentProgressInformation:(CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite];
        }];
246 247 248

}

249
- (void)loadStreamFrom:(NSString *)path
250
{
Carola Nitz's avatar
Carola Nitz committed
251
    if (!path || ![self _supportedFileExtension:[path lastPathComponent]]) {
252 253
        [self _handleError:[NSError errorWithDomain:NSLocalizedString(@"FILE_NOT_SUPPORTED", nil) code:415 userInfo:nil]];
        return;
254
    }
255

256 257 258 259 260
    [[_client.filesRoutes getTemporaryLink:path] setResponseBlock:^(DBFILESGetTemporaryLinkResult * _Nullable result, DBFILESGetTemporaryLinkError * _Nullable routeError, DBRequestError * _Nullable networkError) {

        if (result) {
            VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance];
            [vpc playURL:[NSURL URLWithString:result.link ] successCallback:nil errorCallback:nil];
Carola Nitz's avatar
Carola Nitz committed
261
#if TARGET_OS_TV
262 263 264 265 266 267
            if (_lastKnownNavigationController) {
                VLCFullscreenMovieTVViewController *movieVC = [VLCFullscreenMovieTVViewController fullscreenMovieTVViewController];
                [_lastKnownNavigationController presentViewController:movieVC
                                                             animated:YES
                                                           completion:nil];
            }
Carola Nitz's avatar
Carola Nitz committed
268
#endif
269 270 271 272 273
        } 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]];
        }
    }];
274 275 276 277
}

#pragma mark - VLC internal communication and delegate

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
- (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]];

    NSString  *remaingTime = [formatter stringFromDate:date];
    if ([self.delegate respondsToSelector:@selector(updateRemainingTime:)])
        [self.delegate updateRemainingTime:remaingTime];
}

296 297 298 299 300
- (NSArray *)currentListFiles
{
    return _currentFileList;
}

301 302 303 304 305 306 307 308
- (NSInteger)numberOfFilesWaitingToBeDownloaded
{
    if (_listOfDropboxFilesToDownload)
        return _listOfDropboxFilesToDownload.count;

    return 0;
}

309 310 311
#pragma mark - user feedback
- (void)_handleError:(NSError *)error
{
312
#if TARGET_OS_IOS
313 314 315 316 317
    VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedString(@"ERROR_NUMBER", nil), error.code]
                                                      message:error.localizedDescription
                                                     delegate:self
                                            cancelButtonTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                            otherButtonTitles:nil];
318
    [alert show];
319 320 321 322 323 324 325 326 327 328 329 330
#else
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"ERROR_NUMBER", nil), error.code]
                                                                   message:error.localizedDescription
                                                            preferredStyle:UIAlertControllerStyleAlert];

    UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                                            style:UIAlertActionStyleDestructive
                                                          handler:^(UIAlertAction *action) {
                                                          }];

    [alert addAction:defaultAction];

331
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
332
#endif
333 334
}

335 336 337 338 339
- (void)reset
{
    _currentFileList = nil;
}

340
@end