Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / Model / Yap Storage / OTRMediaItem.m @ ce32510a

History | View | Annotate | Download (14.2 KB)

1
//
2
//  OTRMediaItem.m
3
//  ChatSecure
4
//
5
//  Created by David Chiles on 1/19/15.
6
//  Copyright (c) 2015 Chris Ballinger. All rights reserved.
7
//
8

    
9
#import "OTRMediaItem.h"
10
#import "OTRImages.h"
11
#import "OTRFileItem.h"
12
#import "OTRLog.h"
13
#import "OTRTextItem.h"
14
#import "OTRHTMLItem.h"
15
@import JSQMessagesViewController;
16
@import YapDatabase;
17
@import OTRKit;
18
@import MobileCoreServices;
19
@import OTRAssets;
20
#import "OTRDatabaseManager.h"
21
#import <ChatSecureCore/ChatSecureCore-Swift.h>
22

    
23
static NSString* GetExtensionForMimeType(NSString* mimeType) {
24
    NSCParameterAssert(mimeType.length > 0);
25
    if (!mimeType.length) { return @""; }
26
    NSString *extension = @"";
27
    CFStringRef cfMimeType = (__bridge CFStringRef)mimeType;
28
    CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, cfMimeType, NULL);
29
    if (uti) {
30
        extension = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension));
31
        CFRelease(uti);
32
    }
33
    return extension;
34
}
35

    
36

    
37
@implementation OTRMediaItem
38
@synthesize mimeType = _mimeType;
39
@synthesize parentObjectKey = _parentObjectKey;
40
@synthesize parentObjectCollection = _parentObjectCollection;
41
@dynamic displayText;
42

    
43
- (instancetype) initWithFilename:(NSString *)filename mimeType:(NSString*)mimeType isIncoming:(BOOL)isIncoming {
44
    NSParameterAssert(filename);
45
    if (self = [super init]) {
46
        if (!filename.length) {
47
            filename = @"file";
48
        }
49
        _filename = [filename copy];
50
        _isIncoming = isIncoming;
51
        _transferProgress = 0.0f;
52
        if (!mimeType.length) {
53
            _mimeType = [self.class mimeTypeForFilename:filename];
54
        } else {
55
            NSString *extension = GetExtensionForMimeType(mimeType);
56
            if (![filename.pathExtension isEqualToString:extension]) {
57
                DDLogWarn(@"Given file extension does not match expected extension from mime type: %@ %@", filename.pathExtension, extension);
58
                if (!filename.pathExtension.length && extension.length > 0) {
59
                    _filename = [filename stringByAppendingPathExtension:extension];
60
                    DDLogInfo(@"Created new filename with best guess for file extension: %@", _filename);
61
                }
62
            }
63
            _mimeType = [mimeType copy];
64
        }
65
    }
66
    return self;
67
}
68

    
69
+ (NSString*) mimeTypeForFilename:(NSString*)filename {
70
    // This is to handle the case where we're storing temporary items with the full URL as the filename
71
    NSURL *url = [NSURL URLWithString:filename];
72
    NSString *pathExtension = filename.pathExtension;
73
    if (url) {
74
        pathExtension = url.pathExtension;
75
    }
76
    if (!pathExtension) {
77
        pathExtension = @"";
78
    }
79
    NSString *mimeType = OTRKitGetMimeTypeForExtension(pathExtension);
80
    return mimeType;
81
}
82

    
83
/** Returns the appropriate subclass (OTRImageItem, etc) for incoming file */
84
+ (instancetype) incomingItemWithFilename:(NSString*)filename
85
                                 mimeType:(nullable NSString*)mimeType {
86
    if (!mimeType) {
87
        mimeType = [self mimeTypeForFilename:filename];
88
    }
89
    NSRange imageRange = [mimeType rangeOfString:@"image"];
90
    NSRange audioRange = [mimeType rangeOfString:@"audio"];
91
    NSRange videoRange = [mimeType rangeOfString:@"video"];
92
    NSRange htmlRange = [mimeType rangeOfString:@"text/html"];
93
    NSRange textRange = [mimeType rangeOfString:@"text"];
94
    
95
    OTRMediaItem *mediaItem = nil;
96
    Class mediaClass = nil;
97
    if(audioRange.location == 0) {
98
        mediaClass = [OTRAudioItem class];
99
    } else if (imageRange.location == 0) {
100
        mediaClass = [OTRImageItem class];
101
    } else if (videoRange.location == 0) {
102
        mediaClass = [OTRVideoItem class];
103
    } else if (textRange.location == 0) {
104
        if (htmlRange.location == 0) {
105
            mediaClass = [OTRHTMLItem class];
106
        } else {
107
            mediaClass = [OTRTextItem class];
108
        }
109
    } else {
110
        mediaClass = [OTRFileItem class];
111
    }
112
    
113
    if (mediaClass) {
114
        mediaItem = [[mediaClass alloc] initWithFilename:filename mimeType:mimeType isIncoming:YES];
115
    }
116
    return mediaItem;
117
}
118

    
119
- (NSString*) mimeType {
120
    if (_mimeType) {
121
        return _mimeType;
122
    } else {
123
        return [self.class mimeTypeForFilename:self.filename];
124
    }
125
}
126

    
127
- (void)touchParentMessageWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
128
{
129
    [self touchParentObjectWithTransaction:transaction];
130
    NSString *extensionName = [YapDatabaseConstants extensionName:DatabaseExtensionNameRelationshipExtensionName];
131
    NSString *edgeName = [YapDatabaseConstants edgeName:RelationshipEdgeNameMessageMediaEdgeName];
132
    [[transaction ext:extensionName] enumerateEdgesWithName:edgeName destinationKey:self.uniqueId collection:[[self class] collection] usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop) {
133
        [transaction touchObjectForKey:edge.sourceKey inCollection:edge.sourceCollection];
134
    }];
135
}
136

    
137
- (nullable id) parentObjectWithTransaction:(YapDatabaseReadTransaction*)transaction {
138
    if (!self.parentObjectKey || !self.parentObjectCollection) {
139
        return nil;
140
    }
141
    id parent = [transaction objectForKey:self.parentObjectKey inCollection:self.parentObjectCollection];
142
    return parent;
143
}
144

    
145
- (void) touchParentObjectWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {
146
    if (!self.parentObjectKey || !self.parentObjectCollection) {
147
        return;
148
    }
149
    [transaction touchObjectForKey:self.parentObjectKey inCollection:self.parentObjectCollection];
150
}
151

    
152
- (id<OTRMessageProtocol>)parentMessageWithTransaction:(YapDatabaseReadTransaction *)readTransaction
153
{
154
    id parent = [self parentObjectWithTransaction:readTransaction];
155
    if (parent && [parent conformsToProtocol:@protocol(OTRMessageProtocol)]) {
156
        return parent;
157
    }
158
    __block id<OTRMessageProtocol> message = nil;
159
    NSString *extensionName = [YapDatabaseConstants extensionName:DatabaseExtensionNameRelationshipExtensionName];
160
    NSString *edgeName = [YapDatabaseConstants edgeName:RelationshipEdgeNameMessageMediaEdgeName];
161
    [[readTransaction ext:extensionName] enumerateEdgesWithName:edgeName destinationKey:self.uniqueId collection:[[self class] collection] usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop) {
162
        message = [OTRBaseMessage fetchObjectWithUniqueID:edge.sourceKey transaction:readTransaction];
163
        *stop = YES;
164
    }];
165
    return message;
166
}
167

    
168
#pragma - mark JSQMessageMediaData Methods
169

    
170
- (NSUInteger)mediaHash
171
{
172
    return [self hash];
173
}
174

    
175
- (UIView *)mediaView
176
{
177
    [self fetchMediaData];
178
    UIView *errorView = [self errorView];
179
    if (errorView) { return errorView; }
180
    return nil;
181
}
182

    
183
- (CGSize)mediaViewDisplaySize
184
{
185
    // This is an absolutely terrible way of doing this
186
    if ([self downloadMessage].messageError) {
187
        return CGSizeMake(210.0f, 100.0f);
188
    }
189
    //Taken from JSQMediaItem Example project
190
    if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
191
        return CGSizeMake(315.0f, 225.0f);
192
    }
193
    return CGSizeMake(210.0f, 150.0f);
194
}
195

    
196
- (UIView *)mediaPlaceholderView
197
{
198
    CGSize size = [self mediaViewDisplaySize];
199
    UIView *view = [JSQMessagesMediaPlaceholderView viewWithActivityIndicator];
200
    view.frame = CGRectMake(0.0f, 0.0f, size.width, size.height);
201
    [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:view isOutgoing:!self.isIncoming];
202
    return view;
203
}
204

    
205
- (NSUInteger)hash
206
{
207
    return self.filename.hash;
208
}
209

    
210
- (nullable NSURL*) mediaServerURLWithTransaction:(YapDatabaseReadTransaction*)transaction {
211
    id<OTRMessageProtocol> message = [self parentMessageWithTransaction:transaction];
212
    id<OTRThreadOwner> threadOwner = [message threadOwnerWithTransaction:transaction];
213
    NSString *buddyUniqueId = [threadOwner threadIdentifier];
214
    if (!buddyUniqueId) {
215
        return nil;
216
    }
217
    NSURL *url = [[OTRMediaServer sharedInstance] urlForMediaItem:self buddyUniqueId:buddyUniqueId];
218
    return url;
219
}
220

    
221
+ (nullable instancetype) mediaItemForMessage:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadTransaction*)transaction {
222
    if (!message.messageMediaItemKey.length) {
223
        return nil;
224
    }
225
    OTRMediaItem *item = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction];
226
    return item;
227
}
228

    
229
- (BOOL) shouldFetchMediaData {
230
    return YES;
231
}
232

    
233
- (void) fetchMediaData {
234
    if (![self shouldFetchMediaData]) {
235
        return;
236
    }
237
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
238
        if (![self shouldFetchMediaData]) {
239
            return;
240
        }
241
        // The superview should handle creating the actual imageview
242
        // this code is used to fetch the image from the data store and then cache in ram
243
        __block id<OTRThreadOwner> thread = nil;
244
        __block id<OTRMessageProtocol> message = nil;
245
        [OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
246
            message = [self parentMessageWithTransaction:transaction];
247
            thread = [message threadOwnerWithTransaction:transaction];
248
        }];
249
        if (!message || !thread) {
250
            DDLogError(@"Missing parent message or thread for media message!");
251
            return;
252
        }
253
        NSError *error = nil;
254
        NSData *data = [OTRMediaFileManager.shared dataForItem:self buddyUniqueId:thread.threadIdentifier error:&error];
255
        if(!data.length) {
256
            DDLogError(@"No data found for media item: %@", error);
257
        } else if (![self handleMediaData:data message:message]) {
258
            DDLogError(@"Could not handle display for media item %@", self);
259
        } else {
260
            // Success, touch parent message to display it.
261
            [[OTRDatabaseManager sharedInstance].readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
262
                [self touchParentMessageWithTransaction:transaction];
263
            }];
264
        }
265
    });
266
}
267

    
268
/** Overrideable in subclasses. This is called after data is fetched from db, but before display */
269
- (BOOL) handleMediaData:(NSData*)mediaData message:(id<OTRMessageProtocol>)message {
270
    NSParameterAssert(mediaData.length > 0);
271
    if (!mediaData.length) { return NO; }
272
    return NO;
273
}
274

    
275
/** โš ๏ธ Do not call from within an existing database transaction */
276
- (nullable id<OTRDownloadMessage>) downloadMessage {
277
    __block id<OTRMessageProtocol> message = nil;
278
    [OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
279
        message = [self parentMessageWithTransaction:transaction];
280
    }];
281
    if ([message conformsToProtocol:@protocol(OTRDownloadMessage)]) {
282
        return (id<OTRDownloadMessage>)message;
283
    }
284
    return nil;
285
}
286

    
287
- (UIView*) errorView {
288
    id<OTRDownloadMessage> message = [self downloadMessage];
289
    if (message.messageError) {
290
        MediaDownloadView *downloadView = [MediaDownloadView otr_viewFromNib];
291
        if (!downloadView) {
292
            return nil;
293
        }
294
        [downloadView setMediaItem:self message:message];
295
        downloadView.backgroundColor = [UIColor jsq_messageBubbleLightGrayColor];
296
        CGSize size = [self mediaViewDisplaySize];
297
        downloadView.frame = CGRectMake(0, 0, size.width, size.height);
298
        [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:downloadView isOutgoing:!self.isIncoming];
299
        return downloadView;
300
    }
301
    return nil;
302
}
303

    
304
- (NSString*) displayText {
305
    NSString *item = [NSString stringWithFormat:@"๐Ÿ“ %@", FILE_MESSAGE_STRING()];
306
    if ([self isKindOfClass:[OTRImageItem class]]) {
307
        item = [NSString stringWithFormat:@"๐Ÿ“ท %@", PICTURE_MESSAGE_STRING()];
308
    } else if ([self isKindOfClass:[OTRVideoItem class]]) {
309
        item = [NSString stringWithFormat:@"๐ŸŽฅ %@", VIDEO_MESSAGE_STRING()];
310
    } else if ([self isKindOfClass:[OTRAudioItem class]]) {
311
        item = [NSString stringWithFormat:@"๐Ÿ”Š %@", AUDIO_MESSAGE_STRING()];
312
    } else {
313
        NSURL *url = [NSURL URLWithString:self.filename];
314
        if (url) {
315
            item = [NSString stringWithFormat:@"๐Ÿ”— %@", url.absoluteString];
316
        }
317
    }
318
    return item;
319
}
320

    
321
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
322
    NSMutableDictionary *behaviors = [NSMutableDictionary dictionaryWithDictionary:[super encodingBehaviorsByPropertyKey]];
323
    [behaviors setObject:@(MTLModelEncodingBehaviorExcluded) forKey:NSStringFromSelector(@selector(displayText))];
324
    return behaviors;
325
}
326

    
327

    
328
#pragma - mark YapDatabaseRelationshipNode Methods
329

    
330
- (id)yapDatabaseRelationshipEdgeDeleted:(YapDatabaseRelationshipEdge *)edge withReason:(YDB_NotifyReason)reason
331
{
332
    //#865 Delete File because the parent OTRMessage was deleted
333
    __block id<OTRThreadOwner> thread = nil;
334
    __block id<OTRMessageProtocol> message = nil;
335
    [OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
336
        message = [self parentMessageWithTransaction:transaction];
337
        thread = [message threadOwnerWithTransaction:transaction];
338
    }];
339
    if (!message || !thread) {
340
        DDLogError(@"Missing parent message or thread for media message!");
341
        return nil;
342
    }
343
    
344
    NSString *buddyUniqueId = [thread threadIdentifier];
345
    if (!buddyUniqueId) {
346
        return nil;
347
    }
348
    
349
    [[OTRMediaFileManager sharedInstance] deleteDataForItem:self buddyUniqueId:buddyUniqueId completion:^(BOOL success, NSError *error) {
350
        if (error) {
351
            DDLogError(@"ERROR in deleting data for media item");
352
        }
353
    } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
354
    
355
    return nil;
356
}
357

    
358
#pragma - mark Class Methods
359

    
360
+ (CGSize)normalizeWidth:(CGFloat)width height:(CGFloat)height
361
{
362
    CGFloat maxWidth = 210;
363
    CGFloat maxHeight = 150;
364
    if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
365
        maxWidth = 315;
366
        maxHeight = 225;
367
    }
368
    
369
    float aspectRatio = width / height;
370
    
371
    if (aspectRatio < 1) {
372
        //Taller then wider then use max height and resize width
373
        CGFloat newWidth = floorf(maxHeight * aspectRatio);
374
        return CGSizeMake(newWidth, maxHeight);
375
    }
376
    else {
377
        //Wider than taller then use max width and resize height
378
        CGFloat newHeight = floorf(maxWidth * 1/aspectRatio);
379
        return CGSizeMake(maxWidth, newHeight);
380
    }
381
}
382

    
383
@end