Initial commit
This commit is contained in:
@ -0,0 +1,33 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTAudioEngine.h"
|
||||
#import "TPCircularBuffer.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
extern int kBufferLength;
|
||||
extern int kNumberOfChannels;
|
||||
extern int kDefaultSampleRate;
|
||||
extern int kSampleCount_incoming_audio;
|
||||
extern int kSampleCount_outgoing_audio;
|
||||
extern int kBitsPerByte;
|
||||
extern int kFramesPerPacket;
|
||||
extern int kBytesPerSample;
|
||||
extern int kNumberOfAudioQueueBuffers;
|
||||
|
||||
@class OCTAudioQueue;
|
||||
@interface OCTAudioEngine ()
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
@property (strong, nonatomic, readonly) NSString *inputDeviceID;
|
||||
@property (strong, nonatomic, readonly) NSString *outputDeviceID;
|
||||
#endif
|
||||
|
||||
@property (nonatomic, strong) OCTAudioQueue *outputQueue;
|
||||
@property (nonatomic, strong) OCTAudioQueue *inputQueue;
|
||||
|
||||
- (void)makeQueues:(NSError **)error;
|
||||
|
||||
@end
|
@ -0,0 +1,92 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxAV.h"
|
||||
|
||||
@interface OCTAudioEngine : NSObject
|
||||
|
||||
@property (weak, nonatomic) OCTToxAV *toxav;
|
||||
@property (nonatomic, assign) OCTToxFriendNumber friendNumber;
|
||||
|
||||
/**
|
||||
* YES to send audio frames over to tox, otherwise NO.
|
||||
* Default is YES.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL enableMicrophone;
|
||||
|
||||
/**
|
||||
* Starts the Audio Processing Graph.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)startAudioFlow:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Stops the Audio Processing Graph.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)stopAudioFlow:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Checks if the Audio Graph is processing.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES if Audio Graph is running, otherwise NO.
|
||||
*/
|
||||
- (BOOL)isAudioRunning:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Provide audio data that will be placed in buffer to be played in speaker.
|
||||
* @param pcm An array of audio samples (sample_count * channels elements).
|
||||
* @param sampleCount The number of audio samples per channel in the PCM array.
|
||||
* @param channels Number of audio channels.
|
||||
* @param sampleRate Sampling rate used in this frame.
|
||||
*/
|
||||
- (void)provideAudioFrames:(OCTToxAVPCMData *)pcm sampleCount:(OCTToxAVSampleCount)sampleCount channels:(OCTToxAVChannels)channels sampleRate:(OCTToxAVSampleRate)sampleRate fromFriend:(OCTToxFriendNumber)friendNumber;
|
||||
|
||||
@end
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
@interface OCTAudioEngine (MacDevice)
|
||||
|
||||
/**
|
||||
* Set the input device (not available on iOS).
|
||||
* @param inputDeviceID Core Audio's unique ID for the device. See
|
||||
* public OCTSubmanagerCalls.h for what these should be.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)setInputDeviceID:(NSString *)inputDeviceID error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Set the output device (not available on iOS).
|
||||
* @param outputDeviceID Core Audio's unique ID for the device. See
|
||||
* public OCTSubmanagerCalls.h for what these should be.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)setOutputDeviceID:(NSString *)outputDeviceID error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#else
|
||||
|
||||
@interface OCTAudioEngine (iOSDevice)
|
||||
|
||||
/**
|
||||
* Switch the output to/from the device's speaker.
|
||||
* @param speaker Whether we should use the speaker for output.
|
||||
* @param error If this method returns NO, contains more information on the
|
||||
* underlying error.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
@ -0,0 +1,191 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTAudioEngine+Private.h"
|
||||
#import "OCTToxAV+Private.h"
|
||||
#import "OCTAudioQueue.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
@interface OCTAudioEngine ()
|
||||
|
||||
@property (nonatomic, assign) OCTToxAVSampleRate outputSampleRate;
|
||||
@property (nonatomic, assign) OCTToxAVChannels outputNumberOfChannels;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTAudioEngine
|
||||
|
||||
#pragma mark - LifeCycle
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_outputSampleRate = kDefaultSampleRate;
|
||||
_outputNumberOfChannels = kNumberOfChannels;
|
||||
_enableMicrophone = YES;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - SPI
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)setInputDeviceID:(NSString *)inputDeviceID error:(NSError **)error
|
||||
{
|
||||
// if audio is not active, we can't really be bothered to check that the
|
||||
// device exists; we rely on startAudioFlow: to fail later.
|
||||
if (! self.inputQueue) {
|
||||
_inputDeviceID = inputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([self.inputQueue setDeviceID:inputDeviceID error:error]) {
|
||||
_inputDeviceID = inputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)setOutputDeviceID:(NSString *)outputDeviceID error:(NSError **)error
|
||||
{
|
||||
if (! self.outputQueue) {
|
||||
_outputDeviceID = outputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([self.outputQueue setDeviceID:outputDeviceID error:error]) {
|
||||
_outputDeviceID = outputDeviceID;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
AVAudioSessionPortOverride override;
|
||||
if (speaker) {
|
||||
override = AVAudioSessionPortOverrideSpeaker;
|
||||
}
|
||||
else {
|
||||
override = AVAudioSessionPortOverrideNone;
|
||||
}
|
||||
|
||||
return [session overrideOutputAudioPort:override error:error];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)startAudioFlow:(NSError **)error
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
if (! ([session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:error] &&
|
||||
[session setPreferredSampleRate:kDefaultSampleRate error:error] &&
|
||||
[session setMode:AVAudioSessionModeVoiceChat error:error] &&
|
||||
[session setActive:YES error:error])) {
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
[self makeQueues:error];
|
||||
|
||||
if (! (self.outputQueue && self.inputQueue)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OCTAudioEngine *__weak welf = self;
|
||||
self.inputQueue.sendDataBlock = ^(void *data, OCTToxAVSampleCount samples, OCTToxAVSampleRate rate, OCTToxAVChannels channelCount) {
|
||||
OCTAudioEngine *aoi = welf;
|
||||
|
||||
if (aoi.enableMicrophone) {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
[aoi.toxav sendAudioFrame:data
|
||||
sampleCount:samples
|
||||
channels:channelCount
|
||||
sampleRate:rate
|
||||
toFriend:aoi.friendNumber
|
||||
error:nil];
|
||||
}
|
||||
};
|
||||
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
[self.outputQueue updateSampleRate:self.outputSampleRate numberOfChannels:self.outputNumberOfChannels error:nil];
|
||||
|
||||
if (! [self.inputQueue begin:error] || ! [self.outputQueue begin:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)stopAudioFlow:(NSError **)error
|
||||
{
|
||||
if (! [self.inputQueue stop:error] || ! [self.outputQueue stop:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
BOOL ret = [session setActive:NO error:error];
|
||||
#else
|
||||
BOOL ret = YES;
|
||||
#endif
|
||||
|
||||
self.inputQueue = nil;
|
||||
self.outputQueue = nil;
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (void)provideAudioFrames:(OCTToxAVPCMData *)pcm sampleCount:(OCTToxAVSampleCount)sampleCount channels:(OCTToxAVChannels)channels sampleRate:(OCTToxAVSampleRate)sampleRate fromFriend:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
int32_t len = (int32_t)(channels * sampleCount * sizeof(OCTToxAVPCMData));
|
||||
TPCircularBuffer *buf = [self.outputQueue getBufferPointer];
|
||||
if (buf) {
|
||||
TPCircularBufferProduceBytes(buf, pcm, len);
|
||||
}
|
||||
|
||||
if ((self.outputSampleRate != sampleRate) || (self.outputNumberOfChannels != channels)) {
|
||||
// failure is logged by OCTAudioQueue.
|
||||
[self.outputQueue updateSampleRate:sampleRate numberOfChannels:channels error:nil];
|
||||
|
||||
self.outputSampleRate = sampleRate;
|
||||
self.outputNumberOfChannels = channels;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isAudioRunning:(NSError **)error
|
||||
{
|
||||
return self.inputQueue.running && self.outputQueue.running;
|
||||
}
|
||||
|
||||
- (void)makeQueues:(NSError **)error
|
||||
{
|
||||
// Note: OCTAudioQueue handles the case where the device ids are nil - in that case
|
||||
// we don't set the device explicitly, and the default is used.
|
||||
#if TARGET_OS_IPHONE
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
self.outputQueue = [[OCTAudioQueue alloc] initWithOutputDeviceID:nil error:error];
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
self.inputQueue = [[OCTAudioQueue alloc] initWithInputDeviceID:nil error:error];
|
||||
#else
|
||||
self.outputQueue = [[OCTAudioQueue alloc] initWithOutputDeviceID:self.outputDeviceID error:error];
|
||||
self.inputQueue = [[OCTAudioQueue alloc] initWithInputDeviceID:self.inputDeviceID error:error];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,74 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TPCircularBuffer.h"
|
||||
|
||||
@import AudioToolbox;
|
||||
|
||||
#pragma mark - C declarations
|
||||
|
||||
extern OSStatus (*_AudioQueueAllocateBuffer)(AudioQueueRef inAQ,
|
||||
UInt32 inBufferByteSize,
|
||||
AudioQueueBufferRef *outBuffer);
|
||||
extern OSStatus (*_AudioQueueDispose)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate);
|
||||
extern OSStatus (*_AudioQueueEnqueueBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
UInt32 inNumPacketDescs,
|
||||
const AudioStreamPacketDescription *inPacketDescs);
|
||||
extern OSStatus (*_AudioQueueFreeBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer);
|
||||
extern OSStatus (*_AudioQueueNewInput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueInputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ);
|
||||
extern OSStatus (*_AudioQueueNewOutput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueOutputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ);
|
||||
extern OSStatus (*_AudioQueueSetProperty)(AudioQueueRef inAQ,
|
||||
AudioQueuePropertyID inID,
|
||||
const void *inData,
|
||||
UInt32 inDataSize);
|
||||
extern OSStatus (*_AudioQueueStart)(AudioQueueRef inAQ,
|
||||
const AudioTimeStamp *inStartTime);
|
||||
extern OSStatus (*_AudioQueueStop)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate);
|
||||
#if ! TARGET_OS_IPHONE
|
||||
extern OSStatus (*_AudioObjectGetPropertyData)(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress *inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void *inQualifierData,
|
||||
UInt32 *ioDataSize,
|
||||
void *outData);
|
||||
#endif
|
||||
|
||||
/* no idea what to name this thing, so here it is */
|
||||
@interface OCTAudioQueue : NSObject
|
||||
|
||||
@property (strong, nonatomic, readonly) NSString *deviceID;
|
||||
@property (copy, nonatomic) void (^sendDataBlock)(void *, OCTToxAVSampleCount, OCTToxAVSampleRate, OCTToxAVChannels);
|
||||
@property (assign, nonatomic, readonly) BOOL running;
|
||||
|
||||
- (instancetype)initWithInputDeviceID:(NSString *)devID error:(NSError **)error;
|
||||
- (instancetype)initWithOutputDeviceID:(NSString *)devID error:(NSError **)error;
|
||||
|
||||
- (TPCircularBuffer *)getBufferPointer;
|
||||
- (BOOL)updateSampleRate:(OCTToxAVSampleRate)sampleRate numberOfChannels:(OCTToxAVChannels)numberOfChannels error:(NSError **)err;
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
- (BOOL)setDeviceID:(NSString *)deviceID error:(NSError **)err;
|
||||
#endif
|
||||
|
||||
- (BOOL)begin:(NSError **)error;
|
||||
- (BOOL)stop:(NSError **)error;
|
||||
|
||||
@end
|
@ -0,0 +1,425 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTToxAV.h"
|
||||
#import "OCTAudioQueue.h"
|
||||
#import "TPCircularBuffer.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@import AVFoundation;
|
||||
@import AudioToolbox;
|
||||
|
||||
const int kBufferLength = 384000;
|
||||
const int kNumberOfChannels = 1;
|
||||
const int kDefaultSampleRate = 48000;
|
||||
const int kSampleCount_incoming_audio = 1920;
|
||||
const int kSampleCount_outgoing_audio = (1920 / 2);
|
||||
const int kBitsPerByte = 8;
|
||||
const int kFramesPerPacket = 1;
|
||||
// if you make this too small, the output queue will silently not play,
|
||||
// but you will still get fill callbacks; it's really weird
|
||||
const int kFramesPerOutputBuffer_incoming_audio = kSampleCount_incoming_audio / 4;
|
||||
const int kFramesPerOutputBuffer_outgoing_audio = kSampleCount_outgoing_audio / 4;
|
||||
const int kBytesPerSample = sizeof(SInt16);
|
||||
const int kNumberOfAudioQueueBuffers = 8;
|
||||
|
||||
OSStatus (*_AudioQueueAllocateBuffer)(AudioQueueRef inAQ,
|
||||
UInt32 inBufferByteSize,
|
||||
AudioQueueBufferRef *outBuffer) = AudioQueueAllocateBuffer;
|
||||
OSStatus (*_AudioQueueDispose)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate) = AudioQueueDispose;
|
||||
OSStatus (*_AudioQueueEnqueueBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
UInt32 inNumPacketDescs,
|
||||
const AudioStreamPacketDescription *inPacketDescs) = AudioQueueEnqueueBuffer;
|
||||
OSStatus (*_AudioQueueFreeBuffer)(AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer) = AudioQueueFreeBuffer;
|
||||
OSStatus (*_AudioQueueNewInput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueInputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ) = AudioQueueNewInput;
|
||||
OSStatus (*_AudioQueueNewOutput)(const AudioStreamBasicDescription *inFormat,
|
||||
AudioQueueOutputCallback inCallbackProc,
|
||||
void *inUserData,
|
||||
CFRunLoopRef inCallbackRunLoop,
|
||||
CFStringRef inCallbackRunLoopMode,
|
||||
UInt32 inFlags,
|
||||
AudioQueueRef *outAQ) = AudioQueueNewOutput;
|
||||
OSStatus (*_AudioQueueSetProperty)(AudioQueueRef inAQ,
|
||||
AudioQueuePropertyID inID,
|
||||
const void *inData,
|
||||
UInt32 inDataSize) = AudioQueueSetProperty;
|
||||
OSStatus (*_AudioQueueStart)(AudioQueueRef inAQ,
|
||||
const AudioTimeStamp *inStartTime) = AudioQueueStart;
|
||||
OSStatus (*_AudioQueueStop)(AudioQueueRef inAQ,
|
||||
Boolean inImmediate) = AudioQueueStop;
|
||||
#if ! TARGET_OS_IPHONE
|
||||
OSStatus (*_AudioObjectGetPropertyData)(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress *inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void *inQualifierData,
|
||||
UInt32 *ioDataSize,
|
||||
void *outData) = AudioObjectGetPropertyData;
|
||||
#endif
|
||||
|
||||
static NSError *OCTErrorFromCoreAudioCode(OSStatus resultCode)
|
||||
{
|
||||
return [NSError errorWithDomain:NSOSStatusErrorDomain
|
||||
code:resultCode
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"Consult the CoreAudio header files/google for the meaning of the error code."}];
|
||||
}
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
static NSString *OCTGetSystemAudioDevice(AudioObjectPropertySelector sel, NSError **err)
|
||||
{
|
||||
AudioDeviceID devID = 0;
|
||||
OSStatus ok = 0;
|
||||
UInt32 size = sizeof(AudioDeviceID);
|
||||
AudioObjectPropertyAddress address = {
|
||||
.mSelector = sel,
|
||||
.mScope = kAudioObjectPropertyScopeGlobal,
|
||||
.mElement = kAudioObjectPropertyElementMaster
|
||||
};
|
||||
|
||||
ok = _AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &devID);
|
||||
if (ok != kAudioHardwareNoError) {
|
||||
OCTLogCCError(@"failed AudioObjectGetPropertyData for system object: %d! Crash may or may not be imminent", ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
address.mSelector = kAudioDevicePropertyDeviceUID;
|
||||
CFStringRef unique = NULL;
|
||||
size = sizeof(unique);
|
||||
ok = _AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &unique);
|
||||
if (ok != kAudioHardwareNoError) {
|
||||
OCTLogCCError(@"failed AudioObjectGetPropertyData for selected device: %d! Crash may or may not be imminent", ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (__bridge NSString *)unique;
|
||||
}
|
||||
#endif
|
||||
|
||||
@interface OCTAudioQueue ()
|
||||
|
||||
// use this to track what nil means in terms of audio device
|
||||
@property (assign, nonatomic) BOOL isOutput;
|
||||
@property (assign, nonatomic) AudioStreamBasicDescription streamFmt;
|
||||
@property (assign, nonatomic) AudioQueueRef audioQueue;
|
||||
@property (assign, nonatomic) TPCircularBuffer buffer;
|
||||
@property (assign, nonatomic) BOOL running;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTAudioQueue
|
||||
{
|
||||
AudioQueueBufferRef _AQBuffers[kNumberOfAudioQueueBuffers];
|
||||
}
|
||||
|
||||
- (instancetype)initWithDeviceID:(NSString *)devID isOutput:(BOOL)output error:(NSError **)error
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
_streamFmt.mSampleRate = session.sampleRate;
|
||||
#else
|
||||
_streamFmt.mSampleRate = kDefaultSampleRate;
|
||||
#endif
|
||||
_streamFmt.mFormatID = kAudioFormatLinearPCM;
|
||||
_streamFmt.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
||||
_streamFmt.mChannelsPerFrame = kNumberOfChannels;
|
||||
_streamFmt.mBytesPerFrame = kBytesPerSample * kNumberOfChannels;
|
||||
_streamFmt.mBitsPerChannel = kBitsPerByte * kBytesPerSample;
|
||||
_streamFmt.mFramesPerPacket = kFramesPerPacket;
|
||||
_streamFmt.mBytesPerPacket = kBytesPerSample * kNumberOfChannels * kFramesPerPacket;
|
||||
_isOutput = output;
|
||||
_deviceID = devID;
|
||||
|
||||
TPCircularBufferInit(&_buffer, kBufferLength);
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithInputDeviceID:(NSString *)devID error:(NSError **)error
|
||||
{
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
return [self initWithDeviceID:devID isOutput:NO error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithOutputDeviceID:(NSString *)devID error:(NSError **)error
|
||||
{
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
return [self initWithDeviceID:devID isOutput:YES error:error];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (self.running) {
|
||||
[self stop:nil];
|
||||
}
|
||||
|
||||
if (self.audioQueue) {
|
||||
_AudioQueueDispose(self.audioQueue, true);
|
||||
}
|
||||
|
||||
TPCircularBufferCleanup(&_buffer);
|
||||
}
|
||||
|
||||
- (OSStatus)createAudioQueue
|
||||
{
|
||||
OSStatus err;
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
err = _AudioQueueNewOutput(&_streamFmt, (void *)&FillOutputBuffer, (__bridge void *)self, NULL, kCFRunLoopCommonModes, 0, &_audioQueue);
|
||||
}
|
||||
else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
err = _AudioQueueNewInput(&_streamFmt, (void *)&InputAvailable, (__bridge void *)self, NULL, kCFRunLoopCommonModes, 0, &_audioQueue);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (_deviceID) {
|
||||
err = _AudioQueueSetProperty(self.audioQueue, kAudioQueueProperty_CurrentDevice, &_deviceID, sizeof(CFStringRef));
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
- (BOOL)begin:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"begin");
|
||||
|
||||
if (! self.audioQueue) {
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
OCTLogError(@"Can't create the audio queue again after a presumably failed updateSampleRate:... call.");
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < kNumberOfAudioQueueBuffers; ++i) {
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
_AudioQueueAllocateBuffer(self.audioQueue, kBytesPerSample * kNumberOfChannels * kFramesPerOutputBuffer_incoming_audio, &(_AQBuffers[i]));
|
||||
} else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
_AudioQueueAllocateBuffer(self.audioQueue, kBytesPerSample * kNumberOfChannels * kFramesPerOutputBuffer_outgoing_audio, &(_AQBuffers[i]));
|
||||
}
|
||||
_AudioQueueEnqueueBuffer(self.audioQueue, _AQBuffers[i], 0, NULL);
|
||||
if (self.isOutput) {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
// For some reason we have to fill it with zero or the callback never gets called.
|
||||
FillOutputBuffer(self, self.audioQueue, _AQBuffers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"Allocated buffers; starting now!");
|
||||
OSStatus res = _AudioQueueStart(self.audioQueue, NULL);
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.running = YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)stop:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"stop");
|
||||
OSStatus res = _AudioQueueStop(self.audioQueue, true);
|
||||
if (res != 0) {
|
||||
if (error) {
|
||||
*error = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
for (int i = 0; i < kNumberOfAudioQueueBuffers; ++i) {
|
||||
_AudioQueueFreeBuffer(self.audioQueue, _AQBuffers[i]);
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"Freed buffers");
|
||||
self.running = NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (TPCircularBuffer *)getBufferPointer
|
||||
{
|
||||
return &_buffer;
|
||||
}
|
||||
|
||||
- (BOOL)setDeviceID:(NSString *)deviceID error:(NSError **)err
|
||||
{
|
||||
#if ! TARGET_OS_IPHONE
|
||||
if (deviceID == nil) {
|
||||
OCTLogVerbose(@"using the default device because nil passed to OCTAudioQueue setDeviceID:");
|
||||
deviceID = OCTGetSystemAudioDevice(self.isOutput ?
|
||||
kAudioHardwarePropertyDefaultOutputDevice :
|
||||
kAudioHardwarePropertyDefaultInputDevice, err);
|
||||
if (! deviceID) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL needToRestart = self.running;
|
||||
|
||||
// we need to pause the queue for a sec
|
||||
if (needToRestart && ! [self stop:err]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OSStatus ok = _AudioQueueSetProperty(self.audioQueue, kAudioQueueProperty_CurrentDevice, &deviceID, sizeof(CFStringRef));
|
||||
|
||||
if (ok != 0) {
|
||||
OCTLogError(@"setDeviceID: Error while live setting device to '%@': %d", deviceID, ok);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(ok);
|
||||
}
|
||||
}
|
||||
else {
|
||||
_deviceID = deviceID;
|
||||
OCTLogVerbose(@"Successfully set the device id to %@", deviceID);
|
||||
}
|
||||
|
||||
if ((needToRestart && ! [self begin:err]) || (ok != 0)) {
|
||||
return NO;
|
||||
}
|
||||
else {
|
||||
return YES;
|
||||
}
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)updateSampleRate:(OCTToxAVSampleRate)sampleRate numberOfChannels:(OCTToxAVChannels)numberOfChannels error:(NSError **)err
|
||||
{
|
||||
OCTLogVerbose(@"updateSampleRate %u, %u", sampleRate, (unsigned int)numberOfChannels);
|
||||
|
||||
BOOL needToRestart = self.running;
|
||||
|
||||
if (needToRestart && ! [self stop:err]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
AudioQueueRef aq = self.audioQueue;
|
||||
self.audioQueue = nil;
|
||||
_AudioQueueDispose(aq, true);
|
||||
|
||||
_streamFmt.mSampleRate = sampleRate;
|
||||
_streamFmt.mChannelsPerFrame = numberOfChannels;
|
||||
_streamFmt.mBytesPerFrame = kBytesPerSample * numberOfChannels;
|
||||
_streamFmt.mBytesPerPacket = kBytesPerSample * numberOfChannels * kFramesPerPacket;
|
||||
|
||||
OSStatus res = [self createAudioQueue];
|
||||
if (res != 0) {
|
||||
OCTLogError(@"oops, could not recreate the audio queue: %d after samplerate/nc change. enjoy your overflowing buffer", (int)res);
|
||||
if (err) {
|
||||
*err = OCTErrorFromCoreAudioCode(res);
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
else if (needToRestart) {
|
||||
return [self begin:err];
|
||||
}
|
||||
else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid annoying bridge cast in 1st param!
|
||||
static void InputAvailable(OCTAudioQueue *__unsafe_unretained context,
|
||||
AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer,
|
||||
const AudioTimeStamp *inStartTime,
|
||||
UInt32 inNumPackets,
|
||||
const AudioStreamPacketDescription *inPacketDesc)
|
||||
{
|
||||
TPCircularBufferProduceBytes(&(context->_buffer),
|
||||
inBuffer->mAudioData,
|
||||
inBuffer->mAudioDataByteSize);
|
||||
|
||||
int32_t availableBytesToConsume;
|
||||
void *tail = TPCircularBufferTail(&context->_buffer, &availableBytesToConsume);
|
||||
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
int32_t minimalBytesToConsume = kSampleCount_outgoing_audio * kNumberOfChannels * sizeof(SInt16);
|
||||
|
||||
if (context.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
minimalBytesToConsume = kSampleCount_incoming_audio * kNumberOfChannels * sizeof(SInt16);
|
||||
}
|
||||
|
||||
int32_t cyclesToConsume = availableBytesToConsume / minimalBytesToConsume;
|
||||
|
||||
for (int32_t i = 0; i < cyclesToConsume; i++) {
|
||||
if (context.isOutput) {
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
context.sendDataBlock(tail, kSampleCount_incoming_audio, context.streamFmt.mSampleRate, kNumberOfChannels);
|
||||
} else {
|
||||
// TOXAUDIO: -outgoing-audio-
|
||||
context.sendDataBlock(tail, kSampleCount_outgoing_audio, context.streamFmt.mSampleRate, kNumberOfChannels);
|
||||
}
|
||||
TPCircularBufferConsume(&context->_buffer, minimalBytesToConsume);
|
||||
tail = TPCircularBufferTail(&context->_buffer, &availableBytesToConsume);
|
||||
}
|
||||
|
||||
_AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
static void FillOutputBuffer(OCTAudioQueue *__unsafe_unretained context,
|
||||
AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer)
|
||||
{
|
||||
int32_t targetBufferSize = inBuffer->mAudioDataBytesCapacity;
|
||||
SInt16 *targetBuffer = inBuffer->mAudioData;
|
||||
|
||||
int32_t availableBytes;
|
||||
SInt16 *buffer = TPCircularBufferTail(&context->_buffer, &availableBytes);
|
||||
|
||||
if (buffer) {
|
||||
uint32_t cpy = MIN(availableBytes, targetBufferSize);
|
||||
memcpy(targetBuffer, buffer, cpy);
|
||||
TPCircularBufferConsume(&context->_buffer, cpy);
|
||||
|
||||
if (cpy != targetBufferSize) {
|
||||
memset(targetBuffer + cpy, 0, targetBufferSize - cpy);
|
||||
OCTLogCCWarn(@"warning not enough frames!!!");
|
||||
}
|
||||
inBuffer->mAudioDataByteSize = targetBufferSize;
|
||||
}
|
||||
else {
|
||||
memset(targetBuffer, 0, targetBufferSize);
|
||||
inBuffer->mAudioDataByteSize = targetBufferSize;
|
||||
}
|
||||
|
||||
_AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,77 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTDefaultFileStorage.h"
|
||||
|
||||
@interface OCTDefaultFileStorage ()
|
||||
|
||||
@property (copy, nonatomic) NSString *saveFileName;
|
||||
@property (copy, nonatomic) NSString *baseDirectory;
|
||||
@property (copy, nonatomic) NSString *temporaryDirectory;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTDefaultFileStorage
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithBaseDirectory:(NSString *)baseDirectory temporaryDirectory:(NSString *)temporaryDirectory
|
||||
{
|
||||
return [self initWithToxSaveFileName:nil baseDirectory:baseDirectory temporaryDirectory:temporaryDirectory];
|
||||
}
|
||||
|
||||
- (instancetype)initWithToxSaveFileName:(NSString *)saveFileName
|
||||
baseDirectory:(NSString *)baseDirectory
|
||||
temporaryDirectory:(NSString *)temporaryDirectory
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (! saveFileName) {
|
||||
saveFileName = @"save";
|
||||
}
|
||||
|
||||
self.saveFileName = [saveFileName stringByAppendingString:@".tox"];
|
||||
self.baseDirectory = baseDirectory;
|
||||
self.temporaryDirectory = temporaryDirectory;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - OCTFileStorageProtocol
|
||||
|
||||
- (NSString *)pathForToxSaveFile
|
||||
{
|
||||
return [self.baseDirectory stringByAppendingPathComponent:self.saveFileName];
|
||||
}
|
||||
|
||||
- (NSString *)pathForDatabase
|
||||
{
|
||||
return [self.baseDirectory stringByAppendingPathComponent:@"database"];
|
||||
}
|
||||
|
||||
- (NSString *)pathForDatabaseEncryptionKey
|
||||
{
|
||||
return [self.baseDirectory stringByAppendingPathComponent:@"database.encryptionkey"];
|
||||
}
|
||||
|
||||
- (NSString *)pathForDownloadedFilesDirectory
|
||||
{
|
||||
return [self.baseDirectory stringByAppendingPathComponent:@"files"];
|
||||
}
|
||||
|
||||
- (NSString *)pathForUploadedFilesDirectory
|
||||
{
|
||||
return [self.baseDirectory stringByAppendingPathComponent:@"files"];
|
||||
}
|
||||
|
||||
- (NSString *)pathForTemporaryFilesDirectory
|
||||
{
|
||||
return self.temporaryDirectory;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,51 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTManagerConfiguration.h"
|
||||
#import "OCTDefaultFileStorage.h"
|
||||
|
||||
static NSString *const kDefaultBaseDirectory = @"me.dvor.objcTox";
|
||||
|
||||
@implementation OCTManagerConfiguration
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (instancetype)defaultConfiguration
|
||||
{
|
||||
OCTManagerConfiguration *configuration = [OCTManagerConfiguration new];
|
||||
|
||||
NSString *baseDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
baseDirectory = [baseDirectory stringByAppendingPathComponent:kDefaultBaseDirectory];
|
||||
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:baseDirectory
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:nil];
|
||||
|
||||
configuration.fileStorage = [[OCTDefaultFileStorage alloc] initWithBaseDirectory:baseDirectory
|
||||
temporaryDirectory:NSTemporaryDirectory()];
|
||||
|
||||
configuration.options = [OCTToxOptions new];
|
||||
|
||||
configuration.importToxSaveFromPath = nil;
|
||||
configuration.useFauxOfflineMessaging = YES;
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
#pragma mark - NSCopying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
OCTManagerConfiguration *configuration = [[[self class] allocWithZone:zone] init];
|
||||
|
||||
configuration.fileStorage = self.fileStorage;
|
||||
configuration.options = [self.options copy];
|
||||
configuration.importToxSaveFromPath = [self.importToxSaveFromPath copy];
|
||||
configuration.useFauxOfflineMessaging = self.useFauxOfflineMessaging;
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,112 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "OCTToxConstants.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
|
||||
@class OCTObject;
|
||||
@class OCTFriend;
|
||||
@class OCTChat;
|
||||
@class OCTCall;
|
||||
@class OCTMessageAbstract;
|
||||
@class OCTSettingsStorageObject;
|
||||
@class RLMResults;
|
||||
|
||||
@interface OCTRealmManager : NSObject
|
||||
|
||||
/**
|
||||
* Storage with all objcTox settings.
|
||||
*/
|
||||
@property (strong, nonatomic, readonly) OCTSettingsStorageObject *settingsStorage;
|
||||
|
||||
/**
|
||||
* Migrate unencrypted database to encrypted one.
|
||||
*
|
||||
* @param databasePath Path to unencrypted database.
|
||||
* @param encryptionKey Key used to encrypt database.
|
||||
* @param error Error parameter will be filled in case of failure. It will contain RLMRealm or NSFileManager error.
|
||||
*
|
||||
* @return YES on success, NO on failure.
|
||||
*/
|
||||
+ (BOOL)migrateToEncryptedDatabase:(NSString *)databasePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Create RealmManager.
|
||||
*
|
||||
* @param fileURL path to Realm file. File will be created if it doesn't exist.
|
||||
* @param encryptionKey A 64-byte key to use to encrypt the data, or nil if encryption is not enabled.
|
||||
*/
|
||||
- (instancetype)initWithDatabaseFileURL:(NSURL *)fileURL encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (NSURL *)realmFileURL;
|
||||
|
||||
#pragma mark - Basic methods
|
||||
|
||||
- (id)objectWithUniqueIdentifier:(NSString *)uniqueIdentifier class:(Class)class;
|
||||
|
||||
- (RLMResults *)objectsWithClass:(Class)class predicate:(NSPredicate *)predicate;
|
||||
|
||||
- (void)addObject:(OCTObject *)object;
|
||||
- (void)deleteObject:(OCTObject *)object;
|
||||
|
||||
/*
|
||||
* All realm objects should be updated ONLY using following two methods.
|
||||
*
|
||||
* Specified object will be passed in block.
|
||||
*/
|
||||
- (void)updateObject:(OCTObject *)object withBlock:(void (^)(id theObject))updateBlock;
|
||||
|
||||
- (void)updateObjectsWithClass:(Class)class
|
||||
predicate:(NSPredicate *)predicate
|
||||
updateBlock:(void (^)(id theObject))updateBlock;
|
||||
|
||||
#pragma mark - Other methods
|
||||
|
||||
- (OCTFriend *)friendWithPublicKey:(NSString *)publicKey;
|
||||
- (OCTChat *)getOrCreateChatWithFriend:(OCTFriend *)friend;
|
||||
- (OCTCall *)createCallWithChat:(OCTChat *)chat status:(OCTCallStatus)status;
|
||||
|
||||
/**
|
||||
* Gets the current call for the chat if and only if it exists.
|
||||
* This will not create a call object.
|
||||
* @param chat The chat that is related to the call.
|
||||
* @return A call object if it exists, nil if no call is session for this call.
|
||||
*/
|
||||
- (OCTCall *)getCurrentCallForChat:(OCTChat *)chat;
|
||||
|
||||
- (void)removeMessages:(NSArray<OCTMessageAbstract *> *)messages;
|
||||
- (void)removeAllMessagesInChat:(OCTChat *)chat removeChat:(BOOL)removeChat;
|
||||
|
||||
/**
|
||||
* Converts all the OCTCalls to OCTMessageCalls.
|
||||
* Only use this when first starting the app or during termination.
|
||||
*/
|
||||
- (void)convertAllCallsToMessages;
|
||||
|
||||
- (OCTMessageAbstract *)addMessageWithText:(NSString *)text
|
||||
type:(OCTToxMessageType)type
|
||||
chat:(OCTChat *)chat
|
||||
sender:(OCTFriend *)sender
|
||||
messageId:(OCTToxMessageId)messageId
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
sentPush:(BOOL)sentPush
|
||||
tssent:(UInt32)tssent
|
||||
tsrcvd:(UInt32)tsrcvd;
|
||||
|
||||
- (OCTMessageAbstract *)addMessageWithFileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileType:(OCTMessageFileType)fileType
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
fileName:(NSString *)fileName
|
||||
filePath:(NSString *)filePath
|
||||
fileUTI:(NSString *)fileUTI
|
||||
chat:(OCTChat *)chat
|
||||
sender:(OCTFriend *)sender;
|
||||
|
||||
- (OCTMessageAbstract *)addMessageCall:(OCTCall *)call;
|
||||
|
||||
@end
|
@ -0,0 +1,665 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Realm/Realm.h>
|
||||
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTFriend.h"
|
||||
#import "OCTFriendRequest.h"
|
||||
#import "OCTChat.h"
|
||||
#import "OCTCall.h"
|
||||
#import "OCTMessageAbstract.h"
|
||||
#import "OCTMessageText.h"
|
||||
#import "OCTMessageFile.h"
|
||||
#import "OCTMessageCall.h"
|
||||
#import "OCTSettingsStorageObject.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
static const uint64_t kCurrentSchemeVersion = 16;
|
||||
static NSString *kSettingsStorageObjectPrimaryKey = @"kSettingsStorageObjectPrimaryKey";
|
||||
|
||||
@interface OCTRealmManager ()
|
||||
|
||||
@property (strong, nonatomic) dispatch_queue_t queue;
|
||||
@property (strong, nonatomic) RLMRealm *realm;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTRealmManager
|
||||
@synthesize settingsStorage = _settingsStorage;
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (BOOL)migrateToEncryptedDatabase:(NSString *)databasePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSString *tempPath = [databasePath stringByAppendingPathExtension:@"tmp"];
|
||||
|
||||
@autoreleasepool {
|
||||
RLMRealm *old = [OCTRealmManager createRealmWithFileURL:[NSURL fileURLWithPath:databasePath]
|
||||
encryptionKey:nil
|
||||
error:error];
|
||||
|
||||
if (! old) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [old writeCopyToURL:[NSURL fileURLWithPath:tempPath] encryptionKey:encryptionKey error:error]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (! [[NSFileManager defaultManager] removeItemAtPath:databasePath error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [[NSFileManager defaultManager] moveItemAtPath:tempPath toPath:databasePath error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithDatabaseFileURL:(NSURL *)fileURL encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
NSParameterAssert(fileURL);
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"init with fileURL %@", fileURL);
|
||||
|
||||
_queue = dispatch_queue_create("OCTRealmManager queue", NULL);
|
||||
|
||||
__weak OCTRealmManager *weakSelf = self;
|
||||
dispatch_sync(_queue, ^{
|
||||
__strong OCTRealmManager *strongSelf = weakSelf;
|
||||
|
||||
// TODO handle error
|
||||
self->_realm = [OCTRealmManager createRealmWithFileURL:fileURL encryptionKey:encryptionKey error:nil];
|
||||
[strongSelf createSettingsStorage];
|
||||
});
|
||||
|
||||
[self convertAllCallsToMessages];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSURL *)realmFileURL
|
||||
{
|
||||
return self.realm.configuration.fileURL;
|
||||
}
|
||||
|
||||
#pragma mark - Basic methods
|
||||
|
||||
- (id)objectWithUniqueIdentifier:(NSString *)uniqueIdentifier class:(Class)class
|
||||
{
|
||||
NSParameterAssert(uniqueIdentifier);
|
||||
NSParameterAssert(class);
|
||||
|
||||
__block OCTObject *object = nil;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
object = [class objectInRealm:self.realm forPrimaryKey:uniqueIdentifier];
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
- (RLMResults *)objectsWithClass:(Class)class predicate:(NSPredicate *)predicate
|
||||
{
|
||||
NSParameterAssert(class);
|
||||
|
||||
__block RLMResults *results;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
results = [class objectsInRealm:self.realm withPredicate:predicate];
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
- (void)updateObject:(OCTObject *)object withBlock:(void (^)(id theObject))updateBlock
|
||||
{
|
||||
NSParameterAssert(object);
|
||||
NSParameterAssert(updateBlock);
|
||||
|
||||
// OCTLogInfo(@"updateObject %@", object);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
updateBlock(object);
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateObjectsWithClass:(Class)class
|
||||
predicate:(NSPredicate *)predicate
|
||||
updateBlock:(void (^)(id theObject))updateBlock
|
||||
{
|
||||
NSParameterAssert(class);
|
||||
NSParameterAssert(updateBlock);
|
||||
|
||||
// OCTLogInfo(@"updating objects of class %@ with predicate %@", NSStringFromClass(class), predicate);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
RLMResults *results = [class objectsInRealm:self.realm withPredicate:predicate];
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
for (id object in results) {
|
||||
updateBlock(object);
|
||||
}
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)addObject:(OCTObject *)object
|
||||
{
|
||||
NSParameterAssert(object);
|
||||
|
||||
// OCTLogInfo(@"add object %@", object);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
[self.realm addObject:object];
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)deleteObject:(OCTObject *)object
|
||||
{
|
||||
NSParameterAssert(object);
|
||||
|
||||
// OCTLogInfo(@"delete object %@", object);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
[self.realm deleteObject:object];
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Other methods
|
||||
|
||||
+ (RLMRealm *)createRealmWithFileURL:(NSURL *)fileURL encryptionKey:(NSData *)encryptionKey error:(NSError **)error
|
||||
{
|
||||
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
||||
configuration.fileURL = fileURL;
|
||||
configuration.schemaVersion = kCurrentSchemeVersion;
|
||||
configuration.migrationBlock = [self realmMigrationBlock];
|
||||
configuration.encryptionKey = encryptionKey;
|
||||
|
||||
RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:error];
|
||||
|
||||
if (! realm && error) {
|
||||
OCTLogInfo(@"Cannot create Realm, error %@", *error);
|
||||
}
|
||||
|
||||
return realm;
|
||||
}
|
||||
|
||||
- (void)createSettingsStorage
|
||||
{
|
||||
_settingsStorage = [OCTSettingsStorageObject objectInRealm:self.realm
|
||||
forPrimaryKey:kSettingsStorageObjectPrimaryKey];
|
||||
|
||||
if (! _settingsStorage) {
|
||||
OCTLogInfo(@"no _settingsStorage, creating it");
|
||||
_settingsStorage = [OCTSettingsStorageObject new];
|
||||
_settingsStorage.uniqueIdentifier = kSettingsStorageObjectPrimaryKey;
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
[self.realm addObject:_settingsStorage];
|
||||
[self.realm commitWriteTransaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (OCTFriend *)friendWithPublicKey:(NSString *)publicKey
|
||||
{
|
||||
NSAssert(publicKey, @"Public key should be non-empty.");
|
||||
__block OCTFriend *friend;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
friend = [[OCTFriend objectsInRealm:self.realm where:@"publicKey == %@", publicKey] firstObject];
|
||||
});
|
||||
|
||||
return friend;
|
||||
}
|
||||
|
||||
- (OCTChat *)getOrCreateChatWithFriend:(OCTFriend *)friend
|
||||
{
|
||||
__block OCTChat *chat = nil;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
// TODO add this (friends.@count == 1) condition. Currentry Realm doesn't support collection queries
|
||||
// See https://github.com/realm/realm-cocoa/issues/1490
|
||||
chat = [[OCTChat objectsInRealm:self.realm where:@"ANY friends == %@", friend] firstObject];
|
||||
|
||||
if (chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"creating chat with friend %@", friend);
|
||||
|
||||
chat = [OCTChat new];
|
||||
chat.lastActivityDateInterval = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
[self.realm addObject:chat];
|
||||
[chat.friends addObject:friend];
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
|
||||
return chat;
|
||||
}
|
||||
|
||||
- (OCTCall *)createCallWithChat:(OCTChat *)chat status:(OCTCallStatus)status
|
||||
{
|
||||
__block OCTCall *call = nil;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
|
||||
call = [[OCTCall objectsInRealm:self.realm where:@"chat == %@", chat] firstObject];
|
||||
|
||||
if (call) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"creating call with chat %@", chat);
|
||||
|
||||
call = [OCTCall new];
|
||||
call.status = status;
|
||||
call.chat = chat;
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
[self.realm addObject:call];
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
- (OCTCall *)getCurrentCallForChat:(OCTChat *)chat
|
||||
{
|
||||
__block OCTCall *call = nil;
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
|
||||
call = [[OCTCall objectsInRealm:self.realm where:@"chat == %@", chat] firstObject];
|
||||
});
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
- (void)removeMessages:(NSArray<OCTMessageAbstract *> *)messages
|
||||
{
|
||||
NSParameterAssert(messages);
|
||||
|
||||
OCTLogInfo(@"removing messages %lu", (unsigned long)messages.count);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
NSMutableSet *changedChats = [NSMutableSet new];
|
||||
for (OCTMessageAbstract *message in messages) {
|
||||
[changedChats addObject:message.chatUniqueIdentifier];
|
||||
}
|
||||
|
||||
[self removeMessagesWithSubmessages:messages];
|
||||
|
||||
for (NSString *chatUniqueIdentifier in changedChats) {
|
||||
RLMResults *messages = [OCTMessageAbstract objectsInRealm:self.realm where:@"chatUniqueIdentifier == %@", chatUniqueIdentifier];
|
||||
messages = [messages sortedResultsUsingKeyPath:@"dateInterval" ascending:YES];
|
||||
|
||||
OCTChat *chat = [OCTChat objectInRealm:self.realm forPrimaryKey:chatUniqueIdentifier];
|
||||
chat.lastMessage = messages.lastObject;
|
||||
}
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)removeAllMessagesInChat:(OCTChat *)chat removeChat:(BOOL)removeChat
|
||||
{
|
||||
NSParameterAssert(chat);
|
||||
|
||||
OCTLogInfo(@"removing chat with all messages %@", chat);
|
||||
|
||||
dispatch_sync(self.queue, ^{
|
||||
RLMResults *messages = [OCTMessageAbstract objectsInRealm:self.realm where:@"chatUniqueIdentifier == %@", chat.uniqueIdentifier];
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
|
||||
[self removeMessagesWithSubmessages:messages];
|
||||
if (removeChat) {
|
||||
[self.realm deleteObject:chat];
|
||||
}
|
||||
|
||||
[self.realm commitWriteTransaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)convertAllCallsToMessages
|
||||
{
|
||||
RLMResults *calls = [OCTCall allObjectsInRealm:self.realm];
|
||||
|
||||
OCTLogInfo(@"removing %lu calls", (unsigned long)calls.count);
|
||||
|
||||
for (OCTCall *call in calls) {
|
||||
[self addMessageCall:call];
|
||||
}
|
||||
|
||||
[self.realm beginWriteTransaction];
|
||||
[self.realm deleteObjects:calls];
|
||||
[self.realm commitWriteTransaction];
|
||||
}
|
||||
|
||||
- (OCTMessageAbstract *)addMessageWithText:(NSString *)text
|
||||
type:(OCTToxMessageType)type
|
||||
chat:(OCTChat *)chat
|
||||
sender:(OCTFriend *)sender
|
||||
messageId:(OCTToxMessageId)messageId
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
sentPush:(BOOL)sentPush
|
||||
tssent:(UInt32)tssent
|
||||
tsrcvd:(UInt32)tsrcvd
|
||||
{
|
||||
NSParameterAssert(text);
|
||||
|
||||
OCTLogInfo(@"adding messageText to chat %@", chat);
|
||||
|
||||
OCTMessageText *messageText = [OCTMessageText new];
|
||||
messageText.text = text;
|
||||
messageText.isDelivered = NO;
|
||||
messageText.type = type;
|
||||
messageText.messageId = messageId;
|
||||
messageText.msgv3HashHex = msgv3HashHex;
|
||||
messageText.sentPush = sentPush;
|
||||
|
||||
return [self addMessageAbstractWithChat:chat sender:sender messageText:messageText messageFile:nil messageCall:nil tssent:tssent tsrcvd:tsrcvd];
|
||||
}
|
||||
|
||||
- (OCTMessageAbstract *)addMessageWithFileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileType:(OCTMessageFileType)fileType
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
fileName:(NSString *)fileName
|
||||
filePath:(NSString *)filePath
|
||||
fileUTI:(NSString *)fileUTI
|
||||
chat:(OCTChat *)chat
|
||||
sender:(OCTFriend *)sender
|
||||
{
|
||||
OCTLogInfo(@"adding messageFile to chat %@, fileSize %lld", chat, fileSize);
|
||||
|
||||
OCTMessageFile *messageFile = [OCTMessageFile new];
|
||||
messageFile.internalFileNumber = fileNumber;
|
||||
messageFile.fileType = fileType;
|
||||
messageFile.fileSize = fileSize;
|
||||
messageFile.fileName = fileName;
|
||||
[messageFile internalSetFilePath:filePath];
|
||||
messageFile.fileUTI = fileUTI;
|
||||
|
||||
return [self addMessageAbstractWithChat:chat sender:sender messageText:nil messageFile:messageFile messageCall:nil tssent:0 tsrcvd:0];
|
||||
}
|
||||
|
||||
- (OCTMessageAbstract *)addMessageCall:(OCTCall *)call
|
||||
{
|
||||
OCTLogInfo(@"adding messageCall to call %@", call);
|
||||
|
||||
OCTMessageCallEvent event;
|
||||
switch (call.status) {
|
||||
case OCTCallStatusDialing:
|
||||
case OCTCallStatusRinging:
|
||||
event = OCTMessageCallEventUnanswered;
|
||||
break;
|
||||
case OCTCallStatusActive:
|
||||
event = OCTMessageCallEventAnswered;
|
||||
break;
|
||||
}
|
||||
|
||||
OCTMessageCall *messageCall = [OCTMessageCall new];
|
||||
messageCall.callDuration = call.callDuration;
|
||||
messageCall.callEvent = event;
|
||||
|
||||
return [self addMessageAbstractWithChat:call.chat sender:call.caller messageText:nil messageFile:nil messageCall:messageCall tssent:0 tsrcvd:0];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
+ (RLMMigrationBlock)realmMigrationBlock
|
||||
{
|
||||
return ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
|
||||
if (oldSchemaVersion < 1) {
|
||||
// objcTox version 0.1.0
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 2) {
|
||||
// objcTox version 0.2.1
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 3) {
|
||||
// objcTox version 0.4.0
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 4) {
|
||||
// objcTox version 0.5.0
|
||||
[self doMigrationVersion4:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 5) {
|
||||
// OCTMessageAbstract: chat property replaced with chatUniqueIdentifier
|
||||
[self doMigrationVersion5:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 6) {
|
||||
// OCTSettingsStorageObject: adding genericSettingsData property.
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 7) {
|
||||
[self doMigrationVersion7:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 8) {
|
||||
[self doMigrationVersion8:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 9) {}
|
||||
|
||||
if (oldSchemaVersion < 10) {}
|
||||
|
||||
if (oldSchemaVersion < 11) {
|
||||
[self doMigrationVersion11:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 12) {
|
||||
[self doMigrationVersion12:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 13) {
|
||||
[self doMigrationVersion13:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 14) {
|
||||
[self doMigrationVersion14:migration];
|
||||
}
|
||||
|
||||
if (oldSchemaVersion < 16) {
|
||||
[self doMigrationVersion16:migration];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion4:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTChat.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"enteredText"] = [oldObject[@"enteredText"] length] > 0 ? oldObject[@"enteredText"] : nil;
|
||||
}];
|
||||
|
||||
[migration enumerateObjects:OCTFriend.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"name"] = [oldObject[@"name"] length] > 0 ? oldObject[@"name"] : nil;
|
||||
newObject[@"statusMessage"] = [oldObject[@"statusMessage"] length] > 0 ? oldObject[@"statusMessage"] : nil;
|
||||
}];
|
||||
|
||||
[migration enumerateObjects:OCTFriendRequest.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"message"] = [oldObject[@"message"] length] > 0 ? oldObject[@"message"] : nil;
|
||||
}];
|
||||
|
||||
[migration enumerateObjects:OCTMessageFile.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"fileName"] = [oldObject[@"fileName"] length] > 0 ? oldObject[@"fileName"] : nil;
|
||||
newObject[@"fileUTI"] = [oldObject[@"fileUTI"] length] > 0 ? oldObject[@"fileUTI"] : nil;
|
||||
}];
|
||||
|
||||
[migration enumerateObjects:OCTMessageText.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"text"] = [oldObject[@"text"] length] > 0 ? oldObject[@"text"] : nil;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion5:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTMessageAbstract.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"chatUniqueIdentifier"] = oldObject[@"chat"][@"uniqueIdentifier"];
|
||||
newObject[@"senderUniqueIdentifier"] = oldObject[@"sender"][@"uniqueIdentifier"];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion7:(RLMMigration *)migration
|
||||
{
|
||||
// Before this version OCTMessageText.isDelivered was broken.
|
||||
// See https://github.com/Antidote-for-Tox/objcTox/issues/158
|
||||
//
|
||||
// After update it was fixed + resending of undelivered messages feature was introduced.
|
||||
// This fired resending all messages that were in history for all friends.
|
||||
//
|
||||
// To fix an issue and stop people suffering we mark all outgoing text messages as delivered.
|
||||
|
||||
[migration enumerateObjects:OCTMessageAbstract.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
if (newObject[@"senderUniqueIdentifier"] != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
RLMObject *messageText = newObject[@"messageText"];
|
||||
|
||||
if (! messageText) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageText[@"isDelivered"] = @YES;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion8:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTFriend.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"pushToken"] = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion11:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTFriend.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"msgv3Capability"] = @NO;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion12:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTMessageText.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"msgv3HashHex"] = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion13:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTMessageText.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"sentPush"] = @YES;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion14:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTMessageAbstract.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"tssent"] = @0;
|
||||
newObject[@"tsrcvd"] = @0;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)doMigrationVersion16:(RLMMigration *)migration
|
||||
{
|
||||
[migration enumerateObjects:OCTFriend.className block:^(RLMObject *oldObject, RLMObject *newObject) {
|
||||
newObject[@"capabilities2"] = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Only one of messageText, messageFile or messageCall can be non-nil.
|
||||
*/
|
||||
- (OCTMessageAbstract *)addMessageAbstractWithChat:(OCTChat *)chat
|
||||
sender:(OCTFriend *)sender
|
||||
messageText:(OCTMessageText *)messageText
|
||||
messageFile:(OCTMessageFile *)messageFile
|
||||
messageCall:(OCTMessageCall *)messageCall
|
||||
tssent:(UInt32)tssent
|
||||
tsrcvd:(UInt32)tsrcvd
|
||||
{
|
||||
NSParameterAssert(chat);
|
||||
|
||||
NSAssert( (messageText && ! messageFile && ! messageCall) ||
|
||||
(! messageText && messageFile && ! messageCall) ||
|
||||
(! messageText && ! messageFile && messageCall),
|
||||
@"Wrong options passed. Only one of messageText, messageFile or messageCall should be non-nil.");
|
||||
|
||||
OCTMessageAbstract *messageAbstract = [OCTMessageAbstract new];
|
||||
messageAbstract.dateInterval = [[NSDate date] timeIntervalSince1970];
|
||||
messageAbstract.senderUniqueIdentifier = sender.uniqueIdentifier;
|
||||
messageAbstract.chatUniqueIdentifier = chat.uniqueIdentifier;
|
||||
messageAbstract.tssent = tssent;
|
||||
messageAbstract.tsrcvd = tsrcvd;
|
||||
messageAbstract.messageText = messageText;
|
||||
messageAbstract.messageFile = messageFile;
|
||||
messageAbstract.messageCall = messageCall;
|
||||
|
||||
[self addObject:messageAbstract];
|
||||
|
||||
[self updateObject:chat withBlock:^(OCTChat *theChat) {
|
||||
theChat.lastMessage = messageAbstract;
|
||||
theChat.lastActivityDateInterval = messageAbstract.dateInterval;
|
||||
}];
|
||||
|
||||
return messageAbstract;
|
||||
}
|
||||
|
||||
// Delete an NSArray, RLMArray, or RLMResults of messages from this Realm.
|
||||
- (void)removeMessagesWithSubmessages:(id)messages
|
||||
{
|
||||
for (OCTMessageAbstract *message in messages) {
|
||||
if (message.messageText) {
|
||||
[self.realm deleteObject:message.messageText];
|
||||
}
|
||||
if (message.messageFile) {
|
||||
[self.realm deleteObject:message.messageFile];
|
||||
}
|
||||
if (message.messageCall) {
|
||||
[self.realm deleteObject:message.messageCall];
|
||||
}
|
||||
}
|
||||
|
||||
[self.realm deleteObjects:messages];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,31 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxConstants.h"
|
||||
|
||||
@class OCTMessageAbstract;
|
||||
|
||||
@interface NSError (OCTFile)
|
||||
|
||||
+ (NSError *)sendFileErrorInternalError;
|
||||
+ (NSError *)sendFileErrorCannotReadFile;
|
||||
+ (NSError *)sendFileErrorCannotSaveFileToUploads;
|
||||
+ (NSError *)sendFileErrorFriendNotFound;
|
||||
+ (NSError *)sendFileErrorFriendNotConnected;
|
||||
+ (NSError *)sendFileErrorNameTooLong;
|
||||
+ (NSError *)sendFileErrorTooMany;
|
||||
+ (NSError *)sendFileErrorFromToxFileSendError:(OCTToxErrorFileSend)code;
|
||||
|
||||
+ (NSError *)acceptFileErrorInternalError;
|
||||
+ (NSError *)acceptFileErrorCannotWriteToFile;
|
||||
+ (NSError *)acceptFileErrorFriendNotFound;
|
||||
+ (NSError *)acceptFileErrorFriendNotConnected;
|
||||
+ (NSError *)acceptFileErrorWrongMessage:(OCTMessageAbstract *)message;
|
||||
+ (NSError *)acceptFileErrorFromToxFileSendChunkError:(OCTToxErrorFileSendChunk)code;
|
||||
+ (NSError *)acceptFileErrorFromToxFileControl:(OCTToxErrorFileControl)code;
|
||||
|
||||
+ (NSError *)fileTransferErrorWrongMessage:(OCTMessageAbstract *)message;
|
||||
|
||||
@end
|
@ -0,0 +1,189 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "NSError+OCTFile.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
|
||||
@implementation NSError (OCTFile)
|
||||
|
||||
+ (NSError *)sendFileErrorInternalError
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorInternalError
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Internal error",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorCannotReadFile
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorCannotReadFile
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Cannot read file",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorCannotSaveFileToUploads
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorCannotSaveFileToUploads
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Cannot save send file to uploads folder.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorFriendNotFound
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorFriendNotFound
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Friend to send file to was not found.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorFriendNotConnected
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorFriendNotConnected
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Friend is not connected at the moment.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorNameTooLong
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorFriendNotConnected
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"File name is too long.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorTooMany
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSendFileErrorFriendNotConnected
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Send file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Too many active file transfers.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)sendFileErrorFromToxFileSendError:(OCTToxErrorFileSend)code
|
||||
{
|
||||
switch (code) {
|
||||
case OCTToxErrorFileSendUnknown:
|
||||
return [self sendFileErrorInternalError];
|
||||
case OCTToxErrorFileSendFriendNotFound:
|
||||
return [self sendFileErrorFriendNotFound];
|
||||
case OCTToxErrorFileSendFriendNotConnected:
|
||||
return [self sendFileErrorFriendNotConnected];
|
||||
case OCTToxErrorFileSendNameTooLong:
|
||||
return [self sendFileErrorNameTooLong];
|
||||
case OCTToxErrorFileSendTooMany:
|
||||
return [self sendFileErrorTooMany];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorInternalError
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTAcceptFileErrorInternalError
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Download file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Internal error",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorCannotWriteToFile
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTAcceptFileErrorCannotWriteToFile
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Download file",
|
||||
NSLocalizedFailureReasonErrorKey : @"File is not available for writing.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorFriendNotFound
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTAcceptFileErrorFriendNotFound
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Download file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Friend to send file to was not found.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorFriendNotConnected
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTAcceptFileErrorFriendNotConnected
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Download file",
|
||||
NSLocalizedFailureReasonErrorKey : @"Friend is not connected at the moment.",
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorWrongMessage:(OCTMessageAbstract *)message
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTAcceptFileErrorWrongMessage
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Download file",
|
||||
NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:@"Specified wrong message %@", message],
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorFromToxFileSendChunkError:(OCTToxErrorFileSendChunk)code
|
||||
{
|
||||
switch (code) {
|
||||
case OCTToxErrorFileSendChunkFriendNotFound:
|
||||
return [self acceptFileErrorFriendNotFound];
|
||||
case OCTToxErrorFileSendChunkFriendNotConnected:
|
||||
return [self acceptFileErrorFriendNotConnected];
|
||||
case OCTToxErrorFileSendChunkUnknown:
|
||||
case OCTToxErrorFileSendChunkNotFound:
|
||||
case OCTToxErrorFileSendChunkNotTransferring:
|
||||
case OCTToxErrorFileSendChunkInvalidLength:
|
||||
case OCTToxErrorFileSendChunkSendq:
|
||||
case OCTToxErrorFileSendChunkWrongPosition:
|
||||
return [self acceptFileErrorInternalError];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSError *)acceptFileErrorFromToxFileControl:(OCTToxErrorFileControl)code
|
||||
{
|
||||
switch (code) {
|
||||
case OCTToxErrorFileControlFriendNotFound:
|
||||
return [self acceptFileErrorFriendNotFound];
|
||||
case OCTToxErrorFileControlFriendNotConnected:
|
||||
return [self acceptFileErrorFriendNotConnected];
|
||||
case OCTToxErrorFileControlNotFound:
|
||||
case OCTToxErrorFileControlNotPaused:
|
||||
case OCTToxErrorFileControlDenied:
|
||||
case OCTToxErrorFileControlAlreadyPaused:
|
||||
case OCTToxErrorFileControlSendq:
|
||||
return [self acceptFileErrorInternalError];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSError *)fileTransferErrorWrongMessage:(OCTMessageAbstract *)message
|
||||
{
|
||||
return [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTFileTransferErrorWrongMessage
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Error",
|
||||
NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:@"Specified wrong message %@", message],
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,42 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileBaseOperation.h"
|
||||
|
||||
@interface OCTFileBaseOperation (Private)
|
||||
|
||||
@property (weak, nonatomic, readonly, nullable) OCTTox *tox;
|
||||
|
||||
@property (assign, nonatomic, readonly) OCTToxFriendNumber friendNumber;
|
||||
@property (assign, nonatomic, readonly) OCTToxFileNumber fileNumber;
|
||||
@property (assign, nonatomic, readonly) OCTToxFileSize fileSize;
|
||||
|
||||
/**
|
||||
* Override this method to start custom actions. Call finish when operation is done.
|
||||
*/
|
||||
- (void)operationStarted NS_REQUIRES_SUPER;
|
||||
|
||||
/**
|
||||
* Override this method to do clean up on operation cancellation.
|
||||
*/
|
||||
- (void)operationWasCanceled NS_REQUIRES_SUPER;
|
||||
|
||||
/**
|
||||
* Call this method to change bytes done value.
|
||||
*/
|
||||
- (void)updateBytesDone:(OCTToxFileSize)bytesDone;
|
||||
|
||||
/**
|
||||
* Call this method in case if operation was finished.
|
||||
*/
|
||||
- (void)finishWithSuccess;
|
||||
|
||||
/**
|
||||
* Call this method in case if operation was finished or cancelled with error.
|
||||
*
|
||||
* @param error Pass error if occured, nil on success.
|
||||
*/
|
||||
- (void)finishWithError:(nonnull NSError *)error;
|
||||
|
||||
@end
|
@ -0,0 +1,85 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTTox.h"
|
||||
#import "OCTToxConstants.h"
|
||||
|
||||
@class OCTFileBaseOperation;
|
||||
|
||||
/**
|
||||
* Block to notify about operation progress.
|
||||
*
|
||||
* @param operation Operation that is running.
|
||||
* @param progress Progress of operation. From 0.0 to 1.0.
|
||||
* @param bytesPerSecond Speed of loading.
|
||||
* @param eta Estimated time of finish of loading.
|
||||
*/
|
||||
typedef void (^OCTFileBaseOperationProgressBlock)(OCTFileBaseOperation *__nonnull operation);
|
||||
|
||||
/**
|
||||
* Block to notify about operation success.
|
||||
*
|
||||
* @param operation Operation that is running.
|
||||
* @param filePath Path of file being loaded
|
||||
*/
|
||||
typedef void (^OCTFileBaseOperationSuccessBlock)(OCTFileBaseOperation *__nonnull operation);
|
||||
|
||||
/**
|
||||
* Block to notify about operation failure.
|
||||
*
|
||||
* @param operation Operation that is running.
|
||||
*/
|
||||
typedef void (^OCTFileBaseOperationFailureBlock)(OCTFileBaseOperation *__nonnull operation, NSError *__nonnull error);
|
||||
|
||||
|
||||
@interface OCTFileBaseOperation : NSOperation
|
||||
|
||||
/**
|
||||
* Identifier of operation, unique for all active file operations.
|
||||
*/
|
||||
@property (strong, nonatomic, readonly, nonnull) NSString *operationId;
|
||||
|
||||
/**
|
||||
* Progress properties.
|
||||
*/
|
||||
@property (assign, nonatomic, readonly) OCTToxFileSize bytesDone;
|
||||
@property (assign, nonatomic, readonly) float progress;
|
||||
@property (assign, nonatomic, readonly) OCTToxFileSize bytesPerSecond;
|
||||
@property (assign, nonatomic, readonly) CFTimeInterval eta;
|
||||
|
||||
@property (strong, nonatomic, readonly, nullable) NSDictionary *userInfo;
|
||||
|
||||
/**
|
||||
* Creates operation id from file and friend number.
|
||||
*/
|
||||
+ (nonnull NSString *)operationIdFromFileNumber:(OCTToxFileNumber)fileNumber friendNumber:(OCTToxFriendNumber)friendNumber;
|
||||
|
||||
/**
|
||||
* Create operation.
|
||||
*
|
||||
* @param tox Tox object to load from.
|
||||
* @param friendNumber Number of friend.
|
||||
* @param fileNumber Number of file to load.
|
||||
* @param fileSize Size of file in bytes.
|
||||
* @param userInfo Any object that will be stored by operation.
|
||||
* @param progressBlock Block called to notify about loading progress. Block will be called on main thread.
|
||||
* @param etaUpdateBlock Block called to notify about loading eta update. Block will be called on main thread.
|
||||
* @param successBlock Block called on operation success. Block will be called on main thread.
|
||||
* @param failureBlock Block called on loading error. Block will be called on main thread.
|
||||
*/
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(nullable NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock;
|
||||
|
||||
- (nullable instancetype)init NS_UNAVAILABLE;
|
||||
+ (nullable instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
@ -0,0 +1,286 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileBaseOperation.h"
|
||||
#import "OCTFileBaseOperation+Private.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
static const CFTimeInterval kMinUpdateProgressInterval = 0.1;
|
||||
static const CFTimeInterval kMinUpdateEtaInterval = 1.0;
|
||||
|
||||
@interface OCTEtaObject : NSObject
|
||||
@property (assign, nonatomic) CFTimeInterval deltaTime;
|
||||
@property (assign, nonatomic) OCTToxFileSize deltaBytes;
|
||||
@end
|
||||
|
||||
@implementation OCTEtaObject
|
||||
@end
|
||||
|
||||
@interface OCTFileBaseOperation ()
|
||||
|
||||
@property (assign, atomic) BOOL privateExecuting;
|
||||
@property (assign, atomic) BOOL privateFinished;
|
||||
|
||||
@property (weak, nonatomic, readonly, nullable) OCTTox *tox;
|
||||
|
||||
@property (assign, nonatomic, readonly) OCTToxFriendNumber friendNumber;
|
||||
@property (assign, nonatomic, readonly) OCTToxFileNumber fileNumber;
|
||||
@property (assign, nonatomic, readonly) OCTToxFileSize fileSize;
|
||||
|
||||
@property (assign, nonatomic, readwrite) OCTToxFileSize bytesDone;
|
||||
@property (assign, nonatomic, readwrite) float progress;
|
||||
@property (assign, nonatomic, readwrite) OCTToxFileSize bytesPerSecond;
|
||||
@property (assign, nonatomic, readwrite) CFTimeInterval eta;
|
||||
|
||||
@property (copy, nonatomic) OCTFileBaseOperationProgressBlock progressBlock;
|
||||
@property (copy, nonatomic) OCTFileBaseOperationProgressBlock etaUpdateBlock;
|
||||
@property (copy, nonatomic) OCTFileBaseOperationSuccessBlock successBlock;
|
||||
@property (copy, nonatomic) OCTFileBaseOperationFailureBlock failureBlock;
|
||||
|
||||
@property (assign, nonatomic) CFTimeInterval lastUpdateProgressTime;
|
||||
@property (assign, nonatomic) OCTToxFileSize lastUpdateBytesDone;
|
||||
@property (assign, nonatomic) CFTimeInterval lastUpdateEtaProgressTime;
|
||||
@property (assign, nonatomic) OCTToxFileSize lastUpdateEtaBytesDone;
|
||||
|
||||
@property (strong, nonatomic) NSMutableArray *last10EtaObjects;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFileBaseOperation
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (NSString *)operationIdFromFileNumber:(OCTToxFileNumber)fileNumber friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
return [NSString stringWithFormat:@"%d-%d", fileNumber, friendNumber];
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock
|
||||
{
|
||||
NSParameterAssert(tox);
|
||||
NSParameterAssert(fileSize > 0);
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_operationId = [[self class] operationIdFromFileNumber:fileNumber friendNumber:friendNumber];
|
||||
|
||||
_tox = tox;
|
||||
|
||||
_friendNumber = friendNumber;
|
||||
_fileNumber = fileNumber;
|
||||
_fileSize = fileSize;
|
||||
|
||||
_progress = 0.0;
|
||||
_bytesPerSecond = 0;
|
||||
_eta = 0;
|
||||
|
||||
_userInfo = userInfo;
|
||||
|
||||
_progressBlock = [progressBlock copy];
|
||||
_etaUpdateBlock = [etaUpdateBlock copy];
|
||||
_successBlock = [successBlock copy];
|
||||
_failureBlock = [failureBlock copy];
|
||||
|
||||
_bytesDone = 0;
|
||||
_lastUpdateProgressTime = 0;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setExecuting:(BOOL)executing
|
||||
{
|
||||
[self willChangeValueForKey:@"isExecuting"];
|
||||
self.privateExecuting = executing;
|
||||
[self didChangeValueForKey:@"isExecuting"];
|
||||
}
|
||||
|
||||
- (BOOL)isExecuting
|
||||
{
|
||||
return self.privateExecuting;
|
||||
}
|
||||
|
||||
- (void)setFinished:(BOOL)finished
|
||||
{
|
||||
[self willChangeValueForKey:@"isFinished"];
|
||||
self.privateFinished = finished;
|
||||
[self didChangeValueForKey:@"isFinished"];
|
||||
}
|
||||
|
||||
- (BOOL)isFinished
|
||||
{
|
||||
return self.privateFinished;
|
||||
}
|
||||
|
||||
#pragma mark - Private category
|
||||
|
||||
- (void)updateBytesDone:(OCTToxFileSize)bytesDone
|
||||
{
|
||||
self.bytesDone = bytesDone;
|
||||
|
||||
[self updateProgressIfNeeded:bytesDone];
|
||||
[self updateEtaIfNeeded:bytesDone];
|
||||
}
|
||||
|
||||
- (void)operationStarted
|
||||
{
|
||||
OCTLogInfo(@"start loading file with identifier %@", self.operationId);
|
||||
}
|
||||
|
||||
- (void)operationWasCanceled
|
||||
{
|
||||
OCTLogInfo(@"was cancelled");
|
||||
}
|
||||
|
||||
- (void)finishWithSuccess
|
||||
{
|
||||
OCTLogInfo(@"finished with success");
|
||||
|
||||
self.executing = NO;
|
||||
self.finished = YES;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.successBlock) {
|
||||
self.successBlock(self);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)finishWithError:(nonnull NSError *)error
|
||||
{
|
||||
NSParameterAssert(error);
|
||||
|
||||
OCTLogInfo(@"finished with error %@", error);
|
||||
|
||||
self.executing = NO;
|
||||
self.finished = YES;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.failureBlock) {
|
||||
self.failureBlock(self, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Override
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (self.cancelled) {
|
||||
self.finished = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
self.executing = YES;
|
||||
|
||||
self.lastUpdateProgressTime = CACurrentMediaTime();
|
||||
self.lastUpdateBytesDone = 0;
|
||||
self.lastUpdateEtaProgressTime = CACurrentMediaTime();
|
||||
self.lastUpdateEtaBytesDone = 0;
|
||||
self.last10EtaObjects = [NSMutableArray new];
|
||||
|
||||
[self operationStarted];
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
[super cancel];
|
||||
|
||||
[self operationWasCanceled];
|
||||
|
||||
self.executing = NO;
|
||||
self.finished = YES;
|
||||
}
|
||||
|
||||
- (BOOL)asynchronous
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)updateProgressIfNeeded:(OCTToxFileSize)bytesDone
|
||||
{
|
||||
CFTimeInterval time = CACurrentMediaTime();
|
||||
|
||||
CFTimeInterval deltaTime = time - self.lastUpdateProgressTime;
|
||||
|
||||
if (deltaTime <= kMinUpdateProgressInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.lastUpdateProgressTime = time;
|
||||
self.lastUpdateBytesDone = bytesDone;
|
||||
|
||||
self.progress = (float)bytesDone / self.fileSize;
|
||||
|
||||
OCTLogInfo(@"progress %.2f, bytes per second %lld, eta %.0f seconds", self.progress, self.bytesPerSecond, self.eta);
|
||||
|
||||
if (self.progressBlock) {
|
||||
self.progressBlock(self);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateEtaIfNeeded:(OCTToxFileSize)bytesDone
|
||||
{
|
||||
CFTimeInterval time = CACurrentMediaTime();
|
||||
|
||||
CFTimeInterval deltaTime = time - self.lastUpdateEtaProgressTime;
|
||||
|
||||
if (deltaTime <= kMinUpdateEtaInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCTToxFileSize deltaBytes = bytesDone - self.lastUpdateEtaBytesDone;
|
||||
OCTToxFileSize bytesLeft = self.fileSize - bytesDone;
|
||||
|
||||
self.lastUpdateEtaProgressTime = time;
|
||||
self.lastUpdateEtaBytesDone = bytesDone;
|
||||
|
||||
OCTEtaObject *etaObject = [OCTEtaObject new];
|
||||
etaObject.deltaTime = deltaTime;
|
||||
etaObject.deltaBytes = deltaBytes;
|
||||
|
||||
[self.last10EtaObjects addObject:etaObject];
|
||||
if (self.last10EtaObjects.count > 10) {
|
||||
[self.last10EtaObjects removeObjectAtIndex:0];
|
||||
}
|
||||
|
||||
CFTimeInterval totalDeltaTime = 0.0;
|
||||
OCTToxFileSize totalDeltaBytes = 0;
|
||||
|
||||
for (OCTEtaObject *object in self.last10EtaObjects) {
|
||||
totalDeltaTime += object.deltaTime;
|
||||
totalDeltaBytes += object.deltaBytes;
|
||||
}
|
||||
|
||||
self.bytesPerSecond = totalDeltaBytes / totalDeltaTime;
|
||||
|
||||
if (totalDeltaBytes) {
|
||||
self.eta = totalDeltaTime * bytesLeft / totalDeltaBytes;
|
||||
}
|
||||
|
||||
if (self.etaUpdateBlock) {
|
||||
self.etaUpdateBlock(self);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,15 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTFileInputProtocol.h"
|
||||
|
||||
@interface OCTFileDataInput : NSObject <OCTFileInputProtocol>
|
||||
|
||||
- (nullable instancetype)initWithData:(nonnull NSData *)data;
|
||||
|
||||
- (nullable instancetype)init NS_UNAVAILABLE;
|
||||
+ (nullable instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
@ -0,0 +1,50 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileDataInput.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@interface OCTFileDataInput ()
|
||||
|
||||
@property (strong, nonatomic, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFileDataInput
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (nullable instancetype)initWithData:(nonnull NSData *)data
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_data = data;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - OCTFileInputProtocol
|
||||
|
||||
- (BOOL)prepareToRead
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nonnull NSData *)bytesWithPosition:(OCTToxFileSize)position length:(size_t)length
|
||||
{
|
||||
@try {
|
||||
return [self.data subdataWithRange:NSMakeRange((NSUInteger)position, length)];
|
||||
}
|
||||
@catch (NSException *ex) {
|
||||
OCTLogWarn(@"catched exception %@", ex);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,15 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTFileOutputProtocol.h"
|
||||
|
||||
@interface OCTFileDataOutput : NSObject <OCTFileOutputProtocol>
|
||||
|
||||
/**
|
||||
* Result data. This property will contain data only after download finishes.
|
||||
*/
|
||||
@property (strong, nonatomic, readonly, nullable) NSData *resultData;
|
||||
|
||||
@end
|
@ -0,0 +1,42 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileDataOutput.h"
|
||||
|
||||
@interface OCTFileDataOutput ()
|
||||
|
||||
@property (strong, nonatomic) NSMutableData *tempData;
|
||||
@property (strong, nonatomic) NSData *resultData;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFileDataOutput
|
||||
|
||||
#pragma mark - OCTFileOutputProtocol
|
||||
|
||||
- (BOOL)prepareToWrite
|
||||
{
|
||||
self.tempData = [NSMutableData new];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(nonnull NSData *)data
|
||||
{
|
||||
[self.tempData appendData:data];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)finishWriting
|
||||
{
|
||||
self.resultData = [self.tempData copy];
|
||||
self.tempData = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
self.tempData = nil;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,45 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileBaseOperation.h"
|
||||
|
||||
@class OCTTox;
|
||||
@protocol OCTFileOutputProtocol;
|
||||
|
||||
/**
|
||||
* File operation for downloading file.
|
||||
*
|
||||
* When started will automatically send resume control to friend.
|
||||
*/
|
||||
@interface OCTFileDownloadOperation : OCTFileBaseOperation
|
||||
|
||||
@property (strong, nonatomic, readonly, nonnull) id<OCTFileOutputProtocol> output;
|
||||
|
||||
/**
|
||||
* Create operation.
|
||||
*
|
||||
* @param fileOutput Output to use as a destination for file transfer.
|
||||
*
|
||||
* For other parameters description see OCTFileBaseOperation.
|
||||
*/
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
fileOutput:(nonnull id<OCTFileOutputProtocol>)fileOutput
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(nullable NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock;
|
||||
|
||||
/**
|
||||
* Call this method to get next chunk to operation.
|
||||
*
|
||||
* @param chunk Next chunk of data to append to file.
|
||||
* @param position Position in file to append chunk.
|
||||
*/
|
||||
- (void)receiveChunk:(nullable NSData *)chunk position:(OCTToxFileSize)position;
|
||||
|
||||
@end
|
@ -0,0 +1,111 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileDownloadOperation.h"
|
||||
#import "OCTFileBaseOperation+Private.h"
|
||||
#import "OCTFileOutputProtocol.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "NSError+OCTFile.h"
|
||||
|
||||
@interface OCTFileDownloadOperation ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFileDownloadOperation
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
fileOutput:(nonnull id<OCTFileOutputProtocol>)fileOutput
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock
|
||||
{
|
||||
NSParameterAssert(fileOutput);
|
||||
|
||||
self = [super initWithTox:tox
|
||||
friendNumber:friendNumber
|
||||
fileNumber:fileNumber
|
||||
fileSize:fileSize
|
||||
userInfo:userInfo
|
||||
progressBlock:progressBlock
|
||||
etaUpdateBlock:etaUpdateBlock
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_output = fileOutput;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)receiveChunk:(NSData *)chunk position:(OCTToxFileSize)position
|
||||
{
|
||||
if (! chunk) {
|
||||
if ([self.output finishWriting]) {
|
||||
[self finishWithSuccess];
|
||||
}
|
||||
else {
|
||||
[self finishWithError:[NSError acceptFileErrorCannotWriteToFile]];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.bytesDone != position) {
|
||||
OCTLogWarn(@"bytesDone doesn't match position");
|
||||
[self.tox fileSendControlForFileNumber:self.fileNumber
|
||||
friendNumber:self.friendNumber
|
||||
control:OCTToxFileControlCancel
|
||||
error:nil];
|
||||
[self finishWithError:[NSError acceptFileErrorInternalError]];
|
||||
return;
|
||||
}
|
||||
|
||||
if (! [self.output writeData:chunk]) {
|
||||
[self finishWithError:[NSError acceptFileErrorCannotWriteToFile]];
|
||||
return;
|
||||
}
|
||||
|
||||
[self updateBytesDone:self.bytesDone + chunk.length];
|
||||
}
|
||||
|
||||
#pragma mark - Override
|
||||
|
||||
- (void)operationStarted
|
||||
{
|
||||
[super operationStarted];
|
||||
|
||||
if (! [self.output prepareToWrite]) {
|
||||
[self finishWithError:[NSError acceptFileErrorCannotWriteToFile]];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
if (! [self.tox fileSendControlForFileNumber:self.fileNumber
|
||||
friendNumber:self.friendNumber
|
||||
control:OCTToxFileControlResume
|
||||
error:&error]) {
|
||||
OCTLogWarn(@"cannot send control %@", error);
|
||||
[self finishWithError:[NSError acceptFileErrorFromToxFileControl:error.code]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)operationWasCanceled
|
||||
{
|
||||
[super operationWasCanceled];
|
||||
|
||||
[self.output cancel];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,27 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxConstants.h"
|
||||
|
||||
@protocol OCTFileInputProtocol <NSObject>
|
||||
|
||||
/**
|
||||
* Prepare input to read. This method will be called before first call to bytesWithPosition:length:.
|
||||
*
|
||||
* @return YES on success, NO on failure.
|
||||
*/
|
||||
- (BOOL)prepareToRead;
|
||||
|
||||
/**
|
||||
* Provide bytes.
|
||||
*
|
||||
* @param position Start position to start reading from.
|
||||
* @param length Length of bytes to read.
|
||||
*
|
||||
* @return NSData on success, nil on failure
|
||||
*/
|
||||
- (nonnull NSData *)bytesWithPosition:(OCTToxFileSize)position length:(size_t)length;
|
||||
|
||||
@end
|
@ -0,0 +1,37 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol OCTFileOutputProtocol <NSObject>
|
||||
|
||||
/**
|
||||
* Prepare input to write. This method will be called before first call to writeData:.
|
||||
*
|
||||
* @return YES on success, NO on failure.
|
||||
*/
|
||||
- (BOOL)prepareToWrite;
|
||||
|
||||
/**
|
||||
* Write data to output.
|
||||
*
|
||||
* @param data Data to write.
|
||||
*
|
||||
* @return YES on success, NO on failure.
|
||||
*/
|
||||
- (BOOL)writeData:(nonnull NSData *)data;
|
||||
|
||||
/**
|
||||
* This method is called after last writeData: method.
|
||||
*
|
||||
* @return YES on success, NO on failure.
|
||||
*/
|
||||
- (BOOL)finishWriting;
|
||||
|
||||
/**
|
||||
* This method is called if all progress was canceled. Do needed cleanup.
|
||||
*/
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
@ -0,0 +1,15 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTFileInputProtocol.h"
|
||||
|
||||
@interface OCTFilePathInput : NSObject <OCTFileInputProtocol>
|
||||
|
||||
- (nullable instancetype)initWithFilePath:(nonnull NSString *)filePath;
|
||||
|
||||
- (nullable instancetype)init NS_UNAVAILABLE;
|
||||
+ (nullable instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
@ -0,0 +1,61 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFilePathInput.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@interface OCTFilePathInput ()
|
||||
|
||||
@property (strong, nonatomic, readonly) NSString *filePath;
|
||||
@property (strong, nonatomic) NSFileHandle *handle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFilePathInput
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (nullable instancetype)initWithFilePath:(nonnull NSString *)filePath
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_filePath = filePath;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - OCTFileInputProtocol
|
||||
|
||||
- (BOOL)prepareToRead
|
||||
{
|
||||
self.handle = [NSFileHandle fileHandleForReadingAtPath:self.filePath];
|
||||
|
||||
if (! self.handle) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSData *)bytesWithPosition:(OCTToxFileSize)position length:(size_t)length
|
||||
{
|
||||
@try {
|
||||
if (self.handle.offsetInFile != position) {
|
||||
[self.handle seekToFileOffset:position];
|
||||
}
|
||||
|
||||
return [self.handle readDataOfLength:length];
|
||||
}
|
||||
@catch (NSException *ex) {
|
||||
OCTLogWarn(@"catched exception %@", ex);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,19 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTFileOutputProtocol.h"
|
||||
|
||||
@interface OCTFilePathOutput : NSObject <OCTFileOutputProtocol>
|
||||
|
||||
@property (copy, nonatomic, readonly, nonnull) NSString *resultFilePath;
|
||||
|
||||
- (nullable instancetype)initWithTempFolder:(nonnull NSString *)tempFolder
|
||||
resultFolder:(nonnull NSString *)resultFolder
|
||||
fileName:(nonnull NSString *)fileName;
|
||||
|
||||
- (nullable instancetype)init NS_UNAVAILABLE;
|
||||
+ (nullable instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
@ -0,0 +1,100 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFilePathOutput.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "OCTFileTools.h"
|
||||
|
||||
@interface OCTFilePathOutput ()
|
||||
|
||||
@property (copy, nonatomic, readonly, nonnull) NSString *tempFilePath;
|
||||
|
||||
@property (strong, nonatomic) NSFileHandle *handle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFilePathOutput
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (nullable instancetype)initWithTempFolder:(nonnull NSString *)tempFolder
|
||||
resultFolder:(nonnull NSString *)resultFolder
|
||||
fileName:(nonnull NSString *)fileName
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_tempFilePath = [OCTFileTools createNewFilePathInDirectory:tempFolder fileName:fileName];
|
||||
_resultFilePath = [OCTFileTools createNewFilePathInDirectory:resultFolder fileName:fileName];
|
||||
|
||||
// Create dummy file to reserve fileName.
|
||||
[[NSFileManager defaultManager] createFileAtPath:_resultFilePath contents:[NSData data] attributes:nil];
|
||||
|
||||
OCTLogInfo(@"temp path %@", _tempFilePath);
|
||||
OCTLogInfo(@"result path %@", _resultFilePath);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - OCTFileOutputProtocol
|
||||
|
||||
- (BOOL)prepareToWrite
|
||||
{
|
||||
if (! [[NSFileManager defaultManager] createFileAtPath:self.tempFilePath contents:nil attributes:nil]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.handle = [NSFileHandle fileHandleForWritingAtPath:self.tempFilePath];
|
||||
|
||||
if (! self.handle) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(nonnull NSData *)data
|
||||
{
|
||||
@try {
|
||||
[self.handle writeData:data];
|
||||
return YES;
|
||||
}
|
||||
@catch (NSException *ex) {
|
||||
OCTLogWarn(@"catched exception %@", ex);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)finishWriting
|
||||
{
|
||||
@try {
|
||||
[self.handle synchronizeFile];
|
||||
}
|
||||
@catch (NSException *ex) {
|
||||
OCTLogWarn(@"catched exception %@", ex);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
// Remove dummy file.
|
||||
if (! [fileManager removeItemAtPath:self.resultFilePath error:nil]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [[NSFileManager defaultManager] moveItemAtPath:self.tempFilePath toPath:self.resultFilePath error:nil];
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
self.handle = nil;
|
||||
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.tempFilePath error:nil];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,20 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface OCTFileTools : NSObject
|
||||
|
||||
/**
|
||||
* Creates filePath in directory for given fileName. In case if file already exists appends " N" suffix,
|
||||
* e.g. "file 2.txt", "file 3.txt".
|
||||
*
|
||||
* @param directory Directory part of filePath.
|
||||
* @param fileName Name of the file to use in path.
|
||||
*
|
||||
* @return File path to file that does not exist.
|
||||
*/
|
||||
+ (nonnull NSString *)createNewFilePathInDirectory:(nonnull NSString *)directory fileName:(nonnull NSString *)fileName;
|
||||
|
||||
@end
|
@ -0,0 +1,77 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileTools.h"
|
||||
|
||||
@implementation OCTFileTools
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (nonnull NSString *)createNewFilePathInDirectory:(nonnull NSString *)directory fileName:(nonnull NSString *)fileName
|
||||
{
|
||||
NSParameterAssert(directory);
|
||||
NSParameterAssert(fileName);
|
||||
|
||||
NSString *path = [directory stringByAppendingPathComponent:fileName];
|
||||
|
||||
if (! [self fileExistsAtPath:path]) {
|
||||
return path;
|
||||
}
|
||||
|
||||
NSString *base;
|
||||
NSString *pathExtension = [fileName pathExtension];
|
||||
NSInteger suffix;
|
||||
|
||||
[self getBaseString:&base andSuffix:&suffix fromString:[fileName stringByDeletingPathExtension]];
|
||||
|
||||
while (YES) {
|
||||
NSString *resultName = [base stringByAppendingFormat:@" %ld", (long)suffix];
|
||||
|
||||
if (pathExtension.length > 0) {
|
||||
resultName = [resultName stringByAppendingPathExtension:pathExtension];
|
||||
}
|
||||
|
||||
NSString *path = [directory stringByAppendingPathComponent:resultName];
|
||||
|
||||
if (! [self fileExistsAtPath:path]) {
|
||||
return path;
|
||||
}
|
||||
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
+ (BOOL)fileExistsAtPath:(NSString *)filePath
|
||||
{
|
||||
return [[NSFileManager defaultManager] fileExistsAtPath:filePath];
|
||||
}
|
||||
|
||||
+ (void)getBaseString:(NSString **)baseString andSuffix:(NSInteger *)suffix fromString:(NSString *)original
|
||||
{
|
||||
NSString *tempBase = original;
|
||||
NSInteger tempSuffix = 1;
|
||||
|
||||
NSArray *components = [tempBase componentsSeparatedByString:@" "];
|
||||
|
||||
if (components.count > 1) {
|
||||
NSString *lastComponent = components.lastObject;
|
||||
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:lastComponent];
|
||||
|
||||
if ([[NSCharacterSet decimalDigitCharacterSet] isSupersetOfSet:set]) {
|
||||
tempSuffix = [lastComponent integerValue];
|
||||
|
||||
// -1 for space.
|
||||
NSInteger index = tempBase.length - lastComponent.length - 1;
|
||||
tempBase = [tempBase substringToIndex:index];
|
||||
}
|
||||
}
|
||||
tempSuffix++;
|
||||
|
||||
*baseString = tempBase;
|
||||
*suffix = tempSuffix;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,39 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileBaseOperation.h"
|
||||
|
||||
@protocol OCTFileInputProtocol;
|
||||
|
||||
@interface OCTFileUploadOperation : OCTFileBaseOperation
|
||||
|
||||
@property (strong, nonatomic, readonly, nonnull) id<OCTFileInputProtocol> input;
|
||||
|
||||
/**
|
||||
* Create operation.
|
||||
*
|
||||
* @param fileInput Input to use as a source for file transfer.
|
||||
*
|
||||
* For other parameters description see OCTFileBaseOperation.
|
||||
*/
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
fileInput:(nonnull id<OCTFileInputProtocol>)fileInput
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(nullable NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock;
|
||||
|
||||
/**
|
||||
* Call this method to request next chunk.
|
||||
*
|
||||
* @param position The file or stream position from which to continue reading.
|
||||
* @param length The number of bytes requested for the current chunk.
|
||||
*/
|
||||
- (void)chunkRequestWithPosition:(OCTToxFileSize)position length:(size_t)length;
|
||||
|
||||
@end
|
@ -0,0 +1,109 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFileUploadOperation.h"
|
||||
#import "OCTFileBaseOperation+Private.h"
|
||||
#import "OCTFileInputProtocol.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "NSError+OCTFile.h"
|
||||
|
||||
@interface OCTFileUploadOperation ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFileUploadOperation
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (nullable instancetype)initWithTox:(nonnull OCTTox *)tox
|
||||
fileInput:(nonnull id<OCTFileInputProtocol>)fileInput
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
fileNumber:(OCTToxFileNumber)fileNumber
|
||||
fileSize:(OCTToxFileSize)fileSize
|
||||
userInfo:(nullable NSDictionary *)userInfo
|
||||
progressBlock:(nullable OCTFileBaseOperationProgressBlock)progressBlock
|
||||
etaUpdateBlock:(nullable OCTFileBaseOperationProgressBlock)etaUpdateBlock
|
||||
successBlock:(nullable OCTFileBaseOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTFileBaseOperationFailureBlock)failureBlock
|
||||
{
|
||||
NSParameterAssert(fileInput);
|
||||
|
||||
self = [super initWithTox:tox
|
||||
friendNumber:friendNumber
|
||||
fileNumber:fileNumber
|
||||
fileSize:fileSize
|
||||
userInfo:userInfo
|
||||
progressBlock:progressBlock
|
||||
etaUpdateBlock:etaUpdateBlock
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_input = fileInput;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)chunkRequestWithPosition:(OCTToxFileSize)position length:(size_t)length
|
||||
{
|
||||
if (length == 0) {
|
||||
[self finishWithSuccess];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *data = [self.input bytesWithPosition:position length:length];
|
||||
|
||||
if (! data) {
|
||||
[self finishWithError:[NSError sendFileErrorCannotReadFile]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
|
||||
BOOL result;
|
||||
do {
|
||||
result = [self.tox fileSendChunkForFileNumber:self.fileNumber
|
||||
friendNumber:self.friendNumber
|
||||
position:position
|
||||
data:data
|
||||
error:&error];
|
||||
|
||||
if (! result) {
|
||||
[NSThread sleepForTimeInterval:0.01];
|
||||
}
|
||||
}
|
||||
while (! result && error.code == OCTToxErrorFileSendChunkSendq);
|
||||
|
||||
if (! result) {
|
||||
OCTLogWarn(@"upload error %@", error);
|
||||
|
||||
[self.tox fileSendControlForFileNumber:self.fileNumber
|
||||
friendNumber:self.friendNumber
|
||||
control:OCTToxFileControlCancel
|
||||
error:nil];
|
||||
|
||||
[self finishWithError:[NSError acceptFileErrorFromToxFileSendChunkError:error.code]];
|
||||
return;
|
||||
}
|
||||
|
||||
[self updateBytesDone:position + length];
|
||||
}
|
||||
|
||||
#pragma mark - Override
|
||||
|
||||
- (void)operationStarted
|
||||
{
|
||||
[super operationStarted];
|
||||
|
||||
if (! [self.input prepareToRead]) {
|
||||
[self finishWithError:[NSError sendFileErrorCannotReadFile]];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,42 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxConstants.h"
|
||||
|
||||
@class OCTTox;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^OCTSendMessageOperationSuccessBlock)(OCTToxMessageId messageId);
|
||||
typedef void (^OCTSendMessageOperationFailureBlock)(NSError *error);
|
||||
|
||||
@interface OCTSendMessageOperation : NSOperation
|
||||
|
||||
/**
|
||||
* Create operation.
|
||||
*
|
||||
* @param tox Tox object to send to.
|
||||
* @param friendNumber Number of friend to send to.
|
||||
* @param messageType Type of the message to send.
|
||||
* @param message Message to send.
|
||||
* @param msgv3HashHex the messageV3 Hash Hexstring or nil.
|
||||
* @param successBlock Block called on operation success. Block will be called on main thread.
|
||||
* @param failureBlock Block called on loading error. Block will be called on main thread.
|
||||
*/
|
||||
- (instancetype)initWithTox:(OCTTox *)tox
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
messageType:(OCTToxMessageType)messageType
|
||||
message:(NSString *)message
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
msgv3tssec:(UInt32)msgv3tssec
|
||||
successBlock:(nullable OCTSendMessageOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTSendMessageOperationFailureBlock)failureBlock;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,80 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSendMessageOperation.h"
|
||||
#import "OCTTox.h"
|
||||
|
||||
@interface OCTSendMessageOperation ()
|
||||
|
||||
@property (weak, nonatomic, readonly) OCTTox *tox;
|
||||
|
||||
@property (assign, nonatomic, readonly) OCTToxFriendNumber friendNumber;
|
||||
@property (assign, nonatomic, readonly) OCTToxMessageType messageType;
|
||||
@property (copy, nonatomic, readonly) NSString *message;
|
||||
@property (copy, nonatomic, readonly) NSString *msgv3HashHex;
|
||||
@property (assign, nonatomic, readonly) UInt32 msgv3tssec;
|
||||
@property (copy, nonatomic, readonly) OCTSendMessageOperationSuccessBlock successBlock;
|
||||
@property (copy, nonatomic, readonly) OCTSendMessageOperationFailureBlock failureBlock;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTSendMessageOperation
|
||||
|
||||
- (instancetype)initWithTox:(OCTTox *)tox
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
messageType:(OCTToxMessageType)messageType
|
||||
message:(NSString *)message
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
msgv3tssec:(UInt32)msgv3tssec
|
||||
successBlock:(nullable OCTSendMessageOperationSuccessBlock)successBlock
|
||||
failureBlock:(nullable OCTSendMessageOperationFailureBlock)failureBlock
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_tox = tox;
|
||||
_friendNumber = friendNumber;
|
||||
_messageType = messageType;
|
||||
_message = [message copy];
|
||||
_msgv3HashHex = [msgv3HashHex copy];
|
||||
_msgv3tssec = msgv3tssec;
|
||||
_successBlock = [successBlock copy];
|
||||
_failureBlock = [failureBlock copy];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)main
|
||||
{
|
||||
if (self.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
|
||||
OCTToxMessageId messageId = [self.tox sendMessageWithFriendNumber:self.friendNumber
|
||||
type:self.messageType
|
||||
message:self.message
|
||||
msgv3HashHex:self.msgv3HashHex
|
||||
msgv3tssec:self.msgv3tssec
|
||||
error:&error];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error && self.failureBlock) {
|
||||
self.failureBlock(error);
|
||||
}
|
||||
else if (! error && self.successBlock) {
|
||||
self.successBlock(messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,8 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTManagerConstants.h"
|
||||
|
||||
NSString *const kOCTManagerErrorDomain = @"me.dvor.objcTox.OCTManagerErrorDomain";
|
@ -0,0 +1,475 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTManagerFactory.h"
|
||||
#import "OCTManagerImpl.h"
|
||||
#import "OCTManagerConfiguration.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTToxEncryptSave.h"
|
||||
#import "OCTToxEncryptSaveConstants.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, OCTDecryptionErrorFileType) {
|
||||
OCTDecryptionErrorFileTypeDatabaseKey,
|
||||
OCTDecryptionErrorFileTypeToxFile,
|
||||
};
|
||||
|
||||
static const NSUInteger kEncryptedKeyLength = 64;
|
||||
|
||||
@implementation OCTManagerFactory
|
||||
|
||||
+ (void)managerWithConfiguration:(OCTManagerConfiguration *)configuration
|
||||
encryptPassword:(nonnull NSString *)encryptPassword
|
||||
successBlock:(void (^)(id<OCTManager> manager))successBlock
|
||||
failureBlock:(void (^)(NSError *error))failureBlock
|
||||
{
|
||||
[self validateConfiguration:configuration];
|
||||
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
|
||||
// Decrypting Realm.
|
||||
__block NSData *realmEncryptionKey = nil;
|
||||
__block NSError *decryptRealmError = nil;
|
||||
|
||||
dispatch_group_async(group, queue, ^{
|
||||
realmEncryptionKey = [self realmEncryptionKeyWithConfiguration:configuration
|
||||
password:encryptPassword
|
||||
error:&decryptRealmError];
|
||||
});
|
||||
|
||||
|
||||
// Decrypting Tox save.
|
||||
__block NSError *decryptToxError = nil;
|
||||
__block OCTToxEncryptSave *encryptSave = nil;
|
||||
__block NSData *toxSave = nil;
|
||||
|
||||
dispatch_group_async(group, queue, ^{
|
||||
if (! [self importToxSaveIfNeeded:configuration error:&decryptToxError]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *savedData = [self getSavedDataFromPath:configuration.fileStorage.pathForToxSaveFile];
|
||||
|
||||
encryptSave = [self toxEncryptSaveWithToxPassword:encryptPassword savedData:savedData error:&decryptToxError];
|
||||
|
||||
if (! encryptSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
savedData = [self decryptSavedData:savedData encryptSave:encryptSave error:&decryptToxError];
|
||||
|
||||
if (! savedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
toxSave = savedData;
|
||||
});
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSError *error = decryptRealmError ?: decryptToxError;
|
||||
|
||||
if (error) {
|
||||
if (failureBlock) {
|
||||
failureBlock(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
OCTTox *tox = [self createToxWithOptions:configuration.options toxData:toxSave error:&error];
|
||||
|
||||
if (! tox) {
|
||||
if (failureBlock) {
|
||||
failureBlock(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *databaseFileURL = [NSURL fileURLWithPath:configuration.fileStorage.pathForDatabase];
|
||||
OCTRealmManager *realmManager = [[OCTRealmManager alloc] initWithDatabaseFileURL:databaseFileURL encryptionKey:realmEncryptionKey];
|
||||
|
||||
OCTManagerImpl *manager = [[OCTManagerImpl alloc] initWithConfiguration:configuration
|
||||
tox:tox
|
||||
toxEncryptSave:encryptSave
|
||||
realmManager:realmManager];
|
||||
|
||||
if (successBlock) {
|
||||
successBlock(manager);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
+ (void)validateConfiguration:(OCTManagerConfiguration *)configuration
|
||||
{
|
||||
NSParameterAssert(configuration.fileStorage);
|
||||
NSParameterAssert(configuration.fileStorage.pathForDownloadedFilesDirectory);
|
||||
NSParameterAssert(configuration.fileStorage.pathForUploadedFilesDirectory);
|
||||
NSParameterAssert(configuration.fileStorage.pathForTemporaryFilesDirectory);
|
||||
|
||||
NSParameterAssert(configuration.options);
|
||||
}
|
||||
|
||||
+ (NSData *)realmEncryptionKeyWithConfiguration:(OCTManagerConfiguration *)configuration
|
||||
password:(NSString *)databasePassword
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSString *databasePath = configuration.fileStorage.pathForDatabase;
|
||||
NSString *encryptedKeyPath = configuration.fileStorage.pathForDatabaseEncryptionKey;
|
||||
|
||||
BOOL databaseExists = [[NSFileManager defaultManager] fileExistsAtPath:databasePath];
|
||||
BOOL encryptedKeyExists = [[NSFileManager defaultManager] fileExistsAtPath:encryptedKeyPath];
|
||||
|
||||
if (! databaseExists && ! encryptedKeyExists) {
|
||||
// First run, create key and database.
|
||||
if (! [self createEncryptedKeyAtPath:encryptedKeyPath withPassword:databasePassword]) {
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorDatabaseKeyCannotCreateKey];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
if (databaseExists && ! encryptedKeyExists) {
|
||||
// It seems that we found old unencrypted database, let's migrate to encrypted one.
|
||||
NSError *migrationError;
|
||||
|
||||
BOOL result = [self migrateToEncryptedDatabase:databasePath
|
||||
encryptionKeyPath:encryptedKeyPath
|
||||
withPassword:databasePassword
|
||||
error:&migrationError];
|
||||
|
||||
if (! result) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain code:OCTManagerInitErrorDatabaseKeyMigrationToEncryptedFailed userInfo:@{
|
||||
NSLocalizedDescriptionKey : migrationError.localizedDescription,
|
||||
NSLocalizedFailureReasonErrorKey : migrationError.localizedFailureReason,
|
||||
}];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSData *encryptedKey = [NSData dataWithContentsOfFile:encryptedKeyPath];
|
||||
|
||||
if (! encryptedKey) {
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorDatabaseKeyCannotReadKey];
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *decryptError;
|
||||
NSData *key = [OCTToxEncryptSave decryptData:encryptedKey withPassphrase:databasePassword error:&decryptError];
|
||||
|
||||
if (! key) {
|
||||
[self fillError:error withDecryptionError:decryptError.code fileType:OCTDecryptionErrorFileTypeDatabaseKey];
|
||||
return nil;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
+ (BOOL)createEncryptedKeyAtPath:(NSString *)path withPassword:(NSString *)password
|
||||
{
|
||||
NSMutableData *key = [NSMutableData dataWithLength:kEncryptedKeyLength];
|
||||
SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);
|
||||
|
||||
NSData *encryptedKey = [OCTToxEncryptSave encryptData:key withPassphrase:password error:nil];
|
||||
|
||||
return [encryptedKey writeToFile:path options:NSDataWritingAtomic error:nil];
|
||||
}
|
||||
|
||||
+ (BOOL)fillError:(NSError **)error withDecryptionError:(OCTToxEncryptSaveDecryptionError)code fileType:(OCTDecryptionErrorFileType)fileType
|
||||
{
|
||||
if (! error) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDictionary *mapping;
|
||||
|
||||
switch (fileType) {
|
||||
case OCTDecryptionErrorFileTypeDatabaseKey:
|
||||
mapping = @{
|
||||
@(OCTToxEncryptSaveDecryptionErrorNull) : @(OCTManagerInitErrorDatabaseKeyDecryptNull),
|
||||
@(OCTToxEncryptSaveDecryptionErrorBadFormat) : @(OCTManagerInitErrorDatabaseKeyDecryptBadFormat),
|
||||
@(OCTToxEncryptSaveDecryptionErrorFailed) : @(OCTManagerInitErrorDatabaseKeyDecryptFailed),
|
||||
};
|
||||
break;
|
||||
case OCTDecryptionErrorFileTypeToxFile:
|
||||
mapping = @{
|
||||
@(OCTToxEncryptSaveDecryptionErrorNull) : @(OCTManagerInitErrorToxFileDecryptNull),
|
||||
@(OCTToxEncryptSaveDecryptionErrorBadFormat) : @(OCTManagerInitErrorToxFileDecryptBadFormat),
|
||||
@(OCTToxEncryptSaveDecryptionErrorFailed) : @(OCTManagerInitErrorToxFileDecryptFailed),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
OCTManagerInitError initErrorCode = [mapping[@(code)] integerValue];
|
||||
[self fillError:error withInitErrorCode:initErrorCode];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)fillError:(NSError **)error withInitErrorCode:(OCTManagerInitError)code
|
||||
{
|
||||
if (! error) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *failureReason = nil;
|
||||
|
||||
switch (code) {
|
||||
case OCTManagerInitErrorPassphraseFailed:
|
||||
failureReason = @"Cannot create symmetric key from given passphrase.";
|
||||
break;
|
||||
case OCTManagerInitErrorCannotImportToxSave:
|
||||
failureReason = @"Cannot copy tox save at `importToxSaveFromPath` path.";
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyCannotCreateKey:
|
||||
failureReason = @"Cannot create encryption key.";
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyCannotReadKey:
|
||||
failureReason = @"Cannot read encryption key.";
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyMigrationToEncryptedFailed:
|
||||
// Nothing to do here, this error will be created elsewhere.
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyDecryptNull:
|
||||
failureReason = @"Cannot decrypt database key file. Some input data was empty.";
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyDecryptBadFormat:
|
||||
failureReason = @"Cannot decrypt database key file. Data has bad format.";
|
||||
break;
|
||||
case OCTManagerInitErrorDatabaseKeyDecryptFailed:
|
||||
failureReason = @"Cannot decrypt database key file. The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect.";
|
||||
break;
|
||||
case OCTManagerInitErrorToxFileDecryptNull:
|
||||
failureReason = @"Cannot decrypt tox save file. Some input data was empty.";
|
||||
break;
|
||||
case OCTManagerInitErrorToxFileDecryptBadFormat:
|
||||
failureReason = @"Cannot decrypt tox save file. Data has bad format.";
|
||||
break;
|
||||
case OCTManagerInitErrorToxFileDecryptFailed:
|
||||
failureReason = @"Cannot decrypt tox save file. The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxUnknown:
|
||||
failureReason = @"Cannot create tox. Unknown error occurred.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxMemoryError:
|
||||
failureReason = @"Cannot create tox. Was unable to allocate enough memory to store the internal structures for the Tox object.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxPortAlloc:
|
||||
failureReason = @"Cannot create tox. Was unable to bind to a port.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxProxyBadType:
|
||||
failureReason = @"Cannot create tox. Proxy type was invalid.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxProxyBadHost:
|
||||
failureReason = @"Cannot create tox. proxyAddress had an invalid format or was nil (while proxyType was set).";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxProxyBadPort:
|
||||
failureReason = @"Cannot create tox. Proxy port was invalid.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxProxyNotFound:
|
||||
failureReason = @"Cannot create tox. The proxy host passed could not be resolved.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxEncrypted:
|
||||
failureReason = @"Cannot create tox. The saved data to be loaded contained an encrypted save.";
|
||||
break;
|
||||
case OCTManagerInitErrorCreateToxBadFormat:
|
||||
failureReason = @"Cannot create tox. Data has bad format.";
|
||||
break;
|
||||
}
|
||||
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain code:code userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Cannot create OCTManager",
|
||||
NSLocalizedFailureReasonErrorKey : failureReason
|
||||
}];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)migrateToEncryptedDatabase:(NSString *)databasePath
|
||||
encryptionKeyPath:(NSString *)encryptionKeyPath
|
||||
withPassword:(NSString *)password
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(databasePath);
|
||||
NSParameterAssert(encryptionKeyPath);
|
||||
NSParameterAssert(password);
|
||||
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:encryptionKeyPath]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain code:100 userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Cannot migrate unencrypted database to encrypted",
|
||||
NSLocalizedFailureReasonErrorKey : @"Database is already encrypted",
|
||||
}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *tempKeyPath = [encryptionKeyPath stringByAppendingPathExtension:@"tmp"];
|
||||
|
||||
if (! [self createEncryptedKeyAtPath:tempKeyPath withPassword:password]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain code:101 userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Cannot migrate unencrypted database to encrypted",
|
||||
NSLocalizedFailureReasonErrorKey : @"Cannot create encryption key",
|
||||
}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *encryptedKey = [NSData dataWithContentsOfFile:tempKeyPath];
|
||||
|
||||
if (! encryptedKey) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain code:102 userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Cannot migrate unencrypted database to encrypted",
|
||||
NSLocalizedFailureReasonErrorKey : @"Cannot find encryption key",
|
||||
}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *key = [OCTToxEncryptSave decryptData:encryptedKey withPassphrase:password error:error];
|
||||
|
||||
if (! key) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [OCTRealmManager migrateToEncryptedDatabase:databasePath encryptionKey:key error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [[NSFileManager defaultManager] moveItemAtPath:tempKeyPath toPath:encryptionKeyPath error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)importToxSaveIfNeeded:(OCTManagerConfiguration *)configuration error:(NSError **)error
|
||||
{
|
||||
BOOL result = YES;
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
if (configuration.importToxSaveFromPath && [fileManager fileExistsAtPath:configuration.importToxSaveFromPath]) {
|
||||
result = [fileManager copyItemAtPath:configuration.importToxSaveFromPath
|
||||
toPath:configuration.fileStorage.pathForToxSaveFile
|
||||
error:nil];
|
||||
}
|
||||
|
||||
if (! result) {
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCannotImportToxSave];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (OCTToxEncryptSave *)toxEncryptSaveWithToxPassword:(NSString *)toxPassword
|
||||
savedData:(NSData *)savedData
|
||||
error:(NSError **)error
|
||||
{
|
||||
OCTToxEncryptSave *encryptSave;
|
||||
|
||||
if (savedData && [OCTToxEncryptSave isDataEncrypted:savedData]) {
|
||||
encryptSave = [[OCTToxEncryptSave alloc] initWithPassphrase:toxPassword toxData:savedData error:nil];
|
||||
}
|
||||
else {
|
||||
// Save data wasn't encrypted. Passing nil as toxData parameter to encrypt it next time.
|
||||
encryptSave = [[OCTToxEncryptSave alloc] initWithPassphrase:toxPassword toxData:nil error:nil];
|
||||
}
|
||||
|
||||
if (! encryptSave) {
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorPassphraseFailed];
|
||||
return nil;
|
||||
}
|
||||
|
||||
return encryptSave;
|
||||
}
|
||||
|
||||
+ (NSData *)decryptSavedData:(NSData *)data encryptSave:(OCTToxEncryptSave *)encryptSave error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(encryptSave);
|
||||
|
||||
if (! data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (! [OCTToxEncryptSave isDataEncrypted:data]) {
|
||||
// Tox data wasn't encrypted, nothing to do here.
|
||||
return data;
|
||||
}
|
||||
|
||||
NSError *decryptError = nil;
|
||||
|
||||
NSData *result = [encryptSave decryptData:data error:&decryptError];
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
[self fillError:error withDecryptionError:decryptError.code fileType:OCTDecryptionErrorFileTypeToxFile];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (NSData *)getSavedDataFromPath:(NSString *)path
|
||||
{
|
||||
return [[NSFileManager defaultManager] fileExistsAtPath:path] ?
|
||||
([NSData dataWithContentsOfFile:path]) :
|
||||
nil;
|
||||
}
|
||||
|
||||
+ (OCTTox *)createToxWithOptions:(OCTToxOptions *)options toxData:(NSData *)toxData error:(NSError **)error
|
||||
{
|
||||
NSError *toxError = nil;
|
||||
|
||||
OCTTox *tox = [[OCTTox alloc] initWithOptions:options savedData:toxData error:&toxError];
|
||||
|
||||
if (tox) {
|
||||
return tox;
|
||||
}
|
||||
|
||||
OCTToxErrorInitCode code = toxError.code;
|
||||
|
||||
switch (code) {
|
||||
case OCTToxErrorInitCodeUnknown:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxUnknown];
|
||||
break;
|
||||
case OCTToxErrorInitCodeMemoryError:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxMemoryError];
|
||||
break;
|
||||
case OCTToxErrorInitCodePortAlloc:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxPortAlloc];
|
||||
break;
|
||||
case OCTToxErrorInitCodeProxyBadType:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxProxyBadType];
|
||||
break;
|
||||
case OCTToxErrorInitCodeProxyBadHost:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxProxyBadHost];
|
||||
break;
|
||||
case OCTToxErrorInitCodeProxyBadPort:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxProxyBadPort];
|
||||
break;
|
||||
case OCTToxErrorInitCodeProxyNotFound:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxProxyNotFound];
|
||||
break;
|
||||
case OCTToxErrorInitCodeEncrypted:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxEncrypted];
|
||||
break;
|
||||
case OCTToxErrorInitCodeLoadBadFormat:
|
||||
[self fillError:error withInitErrorCode:OCTManagerInitErrorCreateToxBadFormat];
|
||||
break;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,27 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OCTManagerConfiguration;
|
||||
@class OCTTox;
|
||||
@class OCTToxEncryptSave;
|
||||
@class OCTRealmManager;
|
||||
|
||||
@interface OCTManagerImpl : NSObject <OCTManager>
|
||||
|
||||
- (instancetype)initWithConfiguration:(OCTManagerConfiguration *)configuration
|
||||
tox:(OCTTox *)tox
|
||||
toxEncryptSave:(OCTToxEncryptSave *)toxEncryptSave
|
||||
realmManager:(OCTRealmManager *)realmManager;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
347
local_pod_repo/objcTox/Classes/Private/Manager/OCTManagerImpl.m
Normal file
347
local_pod_repo/objcTox/Classes/Private/Manager/OCTManagerImpl.m
Normal file
@ -0,0 +1,347 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "OCTManagerImpl.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTToxEncryptSave.h"
|
||||
#import "OCTManagerConfiguration.h"
|
||||
#import "OCTManagerFactory.h"
|
||||
#import "OCTSubmanagerBootstrapImpl.h"
|
||||
#import "OCTSubmanagerCallsImpl.h"
|
||||
#import "OCTSubmanagerChatsImpl.h"
|
||||
#import "OCTSubmanagerFilesImpl.h"
|
||||
#import "OCTSubmanagerFriendsImpl.h"
|
||||
#import "OCTSubmanagerObjectsImpl.h"
|
||||
#import "OCTSubmanagerUserImpl.h"
|
||||
#import "OCTRealmManager.h"
|
||||
|
||||
@interface OCTManagerImpl () <OCTToxDelegate, OCTSubmanagerDataSource>
|
||||
|
||||
@property (copy, nonatomic, readonly) OCTManagerConfiguration *currentConfiguration;
|
||||
|
||||
@property (strong, nonatomic, readonly) OCTTox *tox;
|
||||
@property (strong, nonatomic, readonly) NSObject *toxSaveFileLock;
|
||||
|
||||
@property (strong, nonatomic, nonnull) OCTToxEncryptSave *encryptSave;
|
||||
|
||||
@property (strong, nonatomic, readonly) OCTRealmManager *realmManager;
|
||||
@property (strong, atomic) NSNotificationCenter *notificationCenter;
|
||||
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerBootstrapImpl *bootstrap;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerCallsImpl *calls;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerChatsImpl *chats;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerFilesImpl *files;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerFriendsImpl *friends;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerObjectsImpl *objects;
|
||||
@property (strong, nonatomic, readwrite) OCTSubmanagerUserImpl *user;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTManagerImpl
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithConfiguration:(OCTManagerConfiguration *)configuration
|
||||
tox:(OCTTox *)tox
|
||||
toxEncryptSave:(OCTToxEncryptSave *)toxEncryptSave
|
||||
realmManager:(OCTRealmManager *)realmManager
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_currentConfiguration = [configuration copy];
|
||||
|
||||
_tox = tox;
|
||||
_tox.delegate = self;
|
||||
_toxSaveFileLock = [NSObject new];
|
||||
|
||||
_encryptSave = toxEncryptSave;
|
||||
|
||||
_realmManager = realmManager;
|
||||
_notificationCenter = [[NSNotificationCenter alloc] init];
|
||||
|
||||
[_tox start];
|
||||
[self saveTox];
|
||||
|
||||
[self createSubmanagers];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self killSubmanagers];
|
||||
[self.tox stop];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (OCTManagerConfiguration *)configuration
|
||||
{
|
||||
return [self.currentConfiguration copy];
|
||||
}
|
||||
|
||||
- (NSString *)exportToxSaveFileAndReturnError:(NSError **)error
|
||||
{
|
||||
@synchronized(self.toxSaveFileLock) {
|
||||
NSString *savedDataPath = self.currentConfiguration.fileStorage.pathForToxSaveFile;
|
||||
NSString *tempPath = self.currentConfiguration.fileStorage.pathForTemporaryFilesDirectory;
|
||||
tempPath = [tempPath stringByAppendingPathComponent:[savedDataPath lastPathComponent]];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
if ([fileManager fileExistsAtPath:tempPath]) {
|
||||
[fileManager removeItemAtPath:tempPath error:error];
|
||||
}
|
||||
|
||||
if (! [fileManager copyItemAtPath:savedDataPath toPath:tempPath error:error]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)changeEncryptPassword:(nonnull NSString *)newPassword oldPassword:(nonnull NSString *)oldPassword
|
||||
{
|
||||
OCTToxEncryptSave *encryptSave = [self changeToxPassword:newPassword oldPassword:oldPassword];
|
||||
if (encryptSave == nil) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [self changeDatabasePassword:newPassword oldPassword:oldPassword]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.encryptSave = encryptSave;
|
||||
[self saveTox];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isManagerEncryptedWithPassword:(nonnull NSString *)password
|
||||
{
|
||||
NSString *toxFilePath = self.currentConfiguration.fileStorage.pathForToxSaveFile;
|
||||
|
||||
return [self isDataAtPath:toxFilePath encryptedWithPassword:password];
|
||||
}
|
||||
|
||||
#pragma mark - OCTSubmanagerDataSource
|
||||
|
||||
- (OCTTox *)managerGetTox
|
||||
{
|
||||
return self.tox;
|
||||
}
|
||||
|
||||
- (BOOL)managerIsToxConnected
|
||||
{
|
||||
return (self.user.connectionStatus != OCTToxConnectionStatusNone);
|
||||
}
|
||||
|
||||
- (void)managerSaveTox
|
||||
{
|
||||
return [self saveTox];
|
||||
}
|
||||
|
||||
- (OCTRealmManager *)managerGetRealmManager
|
||||
{
|
||||
return self.realmManager;
|
||||
}
|
||||
|
||||
- (id<OCTFileStorageProtocol>)managerGetFileStorage
|
||||
{
|
||||
return self.currentConfiguration.fileStorage;
|
||||
}
|
||||
|
||||
- (NSNotificationCenter *)managerGetNotificationCenter
|
||||
{
|
||||
return self.notificationCenter;
|
||||
}
|
||||
|
||||
- (BOOL)managerUseFauxOfflineMessaging
|
||||
{
|
||||
return self.currentConfiguration.useFauxOfflineMessaging;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSData *)getSavedDataFromPath:(NSString *)path
|
||||
{
|
||||
return [[NSFileManager defaultManager] fileExistsAtPath:path] ?
|
||||
([NSData dataWithContentsOfFile:path]) :
|
||||
nil;
|
||||
}
|
||||
|
||||
- (BOOL)isDataAtPath:(NSString *)path encryptedWithPassword:(NSString *)password
|
||||
{
|
||||
NSData *savedData = [self getSavedDataFromPath:path];
|
||||
|
||||
if (! savedData) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([OCTToxEncryptSave isDataEncrypted:savedData]) {
|
||||
return [OCTToxEncryptSave decryptData:savedData withPassphrase:password error:nil] != nil;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)createSubmanagers
|
||||
{
|
||||
_bootstrap = [self createSubmanagerWithClass:[OCTSubmanagerBootstrapImpl class]];
|
||||
_chats = [self createSubmanagerWithClass:[OCTSubmanagerChatsImpl class]];
|
||||
_files = [self createSubmanagerWithClass:[OCTSubmanagerFilesImpl class]];
|
||||
_friends = [self createSubmanagerWithClass:[OCTSubmanagerFriendsImpl class]];
|
||||
_objects = [self createSubmanagerWithClass:[OCTSubmanagerObjectsImpl class]];
|
||||
_user = [self createSubmanagerWithClass:[OCTSubmanagerUserImpl class]];
|
||||
|
||||
OCTSubmanagerCallsImpl *calls = [[OCTSubmanagerCallsImpl alloc] initWithTox:_tox];
|
||||
calls.dataSource = self;
|
||||
_calls = calls;
|
||||
[_calls setupAndReturnError:nil];
|
||||
}
|
||||
|
||||
- (void)killSubmanagers
|
||||
{
|
||||
self.bootstrap = nil;
|
||||
self.calls = nil;
|
||||
self.chats = nil;
|
||||
self.files = nil;
|
||||
self.friends = nil;
|
||||
self.objects = nil;
|
||||
self.user = nil;
|
||||
}
|
||||
|
||||
- (id)createSubmanagerWithClass:(Class)class
|
||||
{
|
||||
id<OCTSubmanagerProtocol> submanager = [[class alloc] init];
|
||||
submanager.dataSource = self;
|
||||
|
||||
if ([submanager respondsToSelector:@selector(configure)]) {
|
||||
[submanager configure];
|
||||
}
|
||||
|
||||
return submanager;
|
||||
}
|
||||
|
||||
- (BOOL)respondsToSelector:(SEL)aSelector
|
||||
{
|
||||
id submanager = [self forwardingTargetForSelector:aSelector];
|
||||
|
||||
if (submanager) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return [super respondsToSelector:aSelector];
|
||||
}
|
||||
|
||||
- (id)forwardingTargetForSelector:(SEL)aSelector
|
||||
{
|
||||
struct objc_method_description description = protocol_getMethodDescription(@protocol(OCTToxDelegate), aSelector, NO, YES);
|
||||
|
||||
if (description.name == NULL) {
|
||||
// We forward methods only from OCTToxDelegate protocol.
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray *submanagers = @[
|
||||
self.bootstrap,
|
||||
self.chats,
|
||||
self.files,
|
||||
self.friends,
|
||||
self.objects,
|
||||
self.user,
|
||||
];
|
||||
|
||||
for (id delegate in submanagers) {
|
||||
if ([delegate respondsToSelector:aSelector]) {
|
||||
return delegate;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)saveTox
|
||||
{
|
||||
@synchronized(self.toxSaveFileLock) {
|
||||
void (^throwException)(NSError *) = ^(NSError *error) {
|
||||
NSDictionary *userInfo = nil;
|
||||
|
||||
if (error) {
|
||||
userInfo = @{ @"NSError" : error };
|
||||
}
|
||||
|
||||
@throw [NSException exceptionWithName:@"saveToxException" reason:error.debugDescription userInfo:userInfo];
|
||||
};
|
||||
|
||||
NSData *data = [self.tox save];
|
||||
|
||||
NSError *error;
|
||||
|
||||
data = [self.encryptSave encryptData:data error:&error];
|
||||
|
||||
if (! data) {
|
||||
throwException(error);
|
||||
}
|
||||
|
||||
if (! [data writeToFile:self.currentConfiguration.fileStorage.pathForToxSaveFile options:NSDataWritingAtomic error:&error]) {
|
||||
throwException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On success returns encryptSave with new password.
|
||||
- (OCTToxEncryptSave *)changeToxPassword:(NSString *)newPassword oldPassword:(NSString *)oldPassword
|
||||
{
|
||||
NSString *toxFilePath = self.currentConfiguration.fileStorage.pathForToxSaveFile;
|
||||
|
||||
if (! [self isDataAtPath:toxFilePath encryptedWithPassword:oldPassword]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block OCTToxEncryptSave *newEncryptSave;
|
||||
|
||||
@synchronized(self.toxSaveFileLock) {
|
||||
// Passing nil as tox data as we are setting new password.
|
||||
newEncryptSave = [[OCTToxEncryptSave alloc] initWithPassphrase:newPassword toxData:nil error:nil];
|
||||
}
|
||||
|
||||
return newEncryptSave;
|
||||
}
|
||||
|
||||
- (BOOL)changeDatabasePassword:(NSString *)newPassword oldPassword:(NSString *)oldPassword
|
||||
{
|
||||
NSParameterAssert(newPassword);
|
||||
NSParameterAssert(oldPassword);
|
||||
|
||||
NSString *encryptedKeyPath = self.currentConfiguration.fileStorage.pathForDatabaseEncryptionKey;
|
||||
NSData *encryptedKey = [NSData dataWithContentsOfFile:encryptedKeyPath];
|
||||
|
||||
if (! encryptedKey) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *key = [OCTToxEncryptSave decryptData:encryptedKey withPassphrase:oldPassword error:nil];
|
||||
|
||||
if (! key) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *newEncryptedKey = [OCTToxEncryptSave encryptData:key withPassphrase:newPassword error:nil];
|
||||
|
||||
if (! newEncryptedKey) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [newEncryptedKey writeToFile:encryptedKeyPath options:NSDataWritingAtomic error:nil];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,11 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTCall.h"
|
||||
|
||||
@interface OCTCall (Utilities)
|
||||
|
||||
- (BOOL)isPaused;
|
||||
|
||||
@end
|
@ -0,0 +1,14 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTCall+Utilities.h"
|
||||
|
||||
@implementation OCTCall (Utilities)
|
||||
|
||||
- (BOOL)isPaused
|
||||
{
|
||||
return (self.pausedStatus != OCTCallPausedStatusNone);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,28 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTCall+Utilities.h"
|
||||
#import "OCTToxAVConstants.h"
|
||||
|
||||
@interface OCTCall ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTCall
|
||||
|
||||
- (BOOL)isOutgoing
|
||||
{
|
||||
return (self.caller == nil);
|
||||
}
|
||||
|
||||
- (NSDate *)onHoldDate
|
||||
{
|
||||
if (self.onHoldStartInterval <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.onHoldStartInterval];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,25 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@class OCTRealmManager;
|
||||
@class OCTCall;
|
||||
|
||||
@interface OCTCallTimer : NSObject
|
||||
|
||||
- (instancetype)initWithRealmManager:(OCTRealmManager *)realmManager;
|
||||
|
||||
/**
|
||||
* Starts the timer for the specified call.
|
||||
* Note that there can only be one active call.
|
||||
* @param call Call to update.
|
||||
*/
|
||||
- (void)startTimerForCall:(OCTCall *)call;
|
||||
|
||||
/**
|
||||
* Stops the timer for the current call in session.
|
||||
*/
|
||||
- (void)stopTimer;
|
||||
|
||||
@end
|
@ -0,0 +1,87 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTCallTimer.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTCall.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@interface OCTCallTimer ()
|
||||
|
||||
@property (strong, nonatomic) dispatch_source_t timer;
|
||||
@property (strong, nonatomic) OCTRealmManager *realmManager;
|
||||
@property (strong, nonatomic) OCTCall *call;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTCallTimer
|
||||
|
||||
- (instancetype)initWithRealmManager:(OCTRealmManager *)realmManager
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_realmManager = realmManager;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)startTimerForCall:(OCTCall *)call
|
||||
{
|
||||
@synchronized(self) {
|
||||
if (self.timer) {
|
||||
NSAssert(! self.timer, @"There is already a timer in progress!");
|
||||
}
|
||||
|
||||
self.call = call;
|
||||
|
||||
// dispatch_queue_t queue = dispatch_queue_create("me.dvor.objcTox.OCTCallQueue", DISPATCH_QUEUE_SERIAL);
|
||||
// Main queue is used temporarily for now since we are getting 'Realm accessed from incorrect thread'.
|
||||
// Should really be using the queue above..
|
||||
|
||||
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
|
||||
uint64_t interval = NSEC_PER_SEC;
|
||||
uint64_t leeway = NSEC_PER_SEC / 1000;
|
||||
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, interval, leeway);
|
||||
|
||||
__weak OCTCallTimer *weakSelf = self;
|
||||
|
||||
dispatch_source_set_event_handler(self.timer, ^{
|
||||
OCTCallTimer *strongSelf = weakSelf;
|
||||
if (! strongSelf) {
|
||||
dispatch_source_cancel(self.timer);
|
||||
OCTLogError(@"Error: Attempt to update timer with no strong pointer to OCTCallTimer");
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf.realmManager updateObject:strongSelf.call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.callDuration += 1.0;
|
||||
}];
|
||||
|
||||
OCTLogInfo(@"Call: %@ duration at %f seconds", strongSelf.call, strongSelf.call.callDuration);
|
||||
});
|
||||
|
||||
dispatch_resume(self.timer);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopTimer
|
||||
{
|
||||
@synchronized(self) {
|
||||
if (! self.timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"Timer for call %@ has stopped at duration %f", self.call, self.call.callDuration);
|
||||
|
||||
dispatch_source_cancel(self.timer);
|
||||
self.timer = nil;
|
||||
self.call = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,39 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTChat.h"
|
||||
#import "OCTMessageAbstract.h"
|
||||
|
||||
@interface OCTChat ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTChat
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSDate *)lastReadDate
|
||||
{
|
||||
if (self.lastReadDateInterval <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.lastReadDateInterval];
|
||||
}
|
||||
|
||||
- (NSDate *)lastActivityDate
|
||||
{
|
||||
if (self.lastActivityDateInterval <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.lastActivityDateInterval];
|
||||
}
|
||||
|
||||
- (BOOL)hasUnreadMessages
|
||||
{
|
||||
return (self.lastMessage.dateInterval > self.lastReadDateInterval);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,41 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFriend.h"
|
||||
|
||||
@interface OCTFriend ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTFriend
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (NSArray *)requiredProperties
|
||||
{
|
||||
NSMutableArray *properties = [NSMutableArray arrayWithArray:[super requiredProperties]];
|
||||
|
||||
[properties addObject:NSStringFromSelector(@selector(nickname))];
|
||||
[properties addObject:NSStringFromSelector(@selector(publicKey))];
|
||||
|
||||
return [properties copy];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSDate *)lastSeenOnline
|
||||
{
|
||||
if (self.lastSeenOnlineInterval <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.lastSeenOnlineInterval];
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"OCTFriend with friendNumber %u, name %@", self.friendNumber, self.name];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,33 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTFriendRequest.h"
|
||||
|
||||
@implementation OCTFriendRequest
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (NSArray *)requiredProperties
|
||||
{
|
||||
NSMutableArray *properties = [NSMutableArray arrayWithArray:[super requiredProperties]];
|
||||
|
||||
[properties addObject:NSStringFromSelector(@selector(publicKey))];
|
||||
|
||||
return [properties copy];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSDate *)date
|
||||
{
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.dateInterval];
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"OCTFriendRequest with publicKey %@...\nmessage length %lu",
|
||||
[self.publicKey substringToIndex:5], (unsigned long)self.message.length];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,49 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTMessageAbstract.h"
|
||||
#import "OCTMessageText.h"
|
||||
#import "OCTMessageFile.h"
|
||||
#import "OCTMessageCall.h"
|
||||
|
||||
@interface OCTMessageAbstract ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTMessageAbstract
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSDate *)date
|
||||
{
|
||||
if (self.dateInterval <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSDate dateWithTimeIntervalSince1970:self.dateInterval];
|
||||
}
|
||||
|
||||
- (BOOL)isOutgoing
|
||||
{
|
||||
return (self.senderUniqueIdentifier == nil);
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *string = nil;
|
||||
|
||||
if (self.messageText) {
|
||||
string = [self.messageText description];
|
||||
}
|
||||
else if (self.messageFile) {
|
||||
string = [self.messageFile description];
|
||||
}
|
||||
else if (self.messageCall) {
|
||||
string = [self.messageCall description];
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"OCTMessageAbstract with date %@, %@", self.date, string];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,34 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTMessageCall.h"
|
||||
|
||||
@implementation OCTMessageCall
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
return [description stringByAppendingString:[self typeDescription]];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSString *)typeDescription
|
||||
{
|
||||
NSString *description;
|
||||
switch (self.callEvent) {
|
||||
case OCTMessageCallEventAnswered:
|
||||
description = [[NSString alloc] initWithFormat:@"Call lasted %f seconds", self.callDuration];
|
||||
break;
|
||||
case OCTMessageCallEventUnanswered:
|
||||
description = @"Call unanswered";
|
||||
break;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,36 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTMessageFile.h"
|
||||
|
||||
@interface OCTMessageFile ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTMessageFile
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (nullable NSString *)filePath
|
||||
{
|
||||
return [self.internalFilePath stringByExpandingTildeInPath];
|
||||
}
|
||||
|
||||
- (void)internalSetFilePath:(NSString *)path
|
||||
{
|
||||
self.internalFilePath = [path stringByAbbreviatingWithTildeInPath];
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
const NSUInteger maxSymbols = 3;
|
||||
NSString *fileName = self.fileName.length > maxSymbols ? ([self.fileName substringToIndex:maxSymbols]) : @"";
|
||||
|
||||
return [description stringByAppendingFormat:@"OCTMessageFile with fileName = %@..., fileSize = %llu",
|
||||
fileName, self.fileSize];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,31 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTMessageText.h"
|
||||
|
||||
@interface OCTMessageText ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTMessageText
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (NSArray *)indexedProperties {
|
||||
return @[@"msgv3HashHex", @"isDelivered", @"sentPush"];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
const NSUInteger maxSymbols = 3;
|
||||
NSString *text = self.text.length > maxSymbols ? ([self.text substringToIndex:maxSymbols]) : @"";
|
||||
|
||||
return [description stringByAppendingFormat:@"OCTMessageText %@..., length %lu", text, (unsigned long)self.text.length];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,30 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "OCTToxConstants.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OCTNode : NSObject
|
||||
|
||||
@property (copy, nonatomic, readonly, nullable) NSString *ipv4Host;
|
||||
@property (copy, nonatomic, readonly, nullable) NSString *ipv6Host;
|
||||
@property (assign, nonatomic, readonly) OCTToxPort udpPort;
|
||||
@property (copy, nonatomic, readonly) NSArray<NSNumber *> *tcpPorts;
|
||||
@property (copy, nonatomic, readonly) NSString *publicKey;
|
||||
|
||||
- (instancetype)initWithIpv4Host:(nullable NSString *)ipv4Host
|
||||
ipv6Host:(nullable NSString *)ipv6Host
|
||||
udpPort:(OCTToxPort)udpPort
|
||||
tcpPorts:(NSArray<NSNumber *> *)tcpPorts
|
||||
publicKey:(NSString *)publicKey;
|
||||
|
||||
- (BOOL)isEqual:(id)object;
|
||||
- (NSUInteger)hash;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,71 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTNode.h"
|
||||
|
||||
@implementation OCTNode
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithIpv4Host:(nullable NSString *)ipv4Host
|
||||
ipv6Host:(nullable NSString *)ipv6Host
|
||||
udpPort:(OCTToxPort)udpPort
|
||||
tcpPorts:(NSArray<NSNumber *> *)tcpPorts
|
||||
publicKey:(NSString *)publicKey
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_ipv4Host = [ipv4Host copy];
|
||||
_ipv6Host = [ipv6Host copy];
|
||||
_udpPort = udpPort;
|
||||
_tcpPorts = [tcpPorts copy];
|
||||
_publicKey = [publicKey copy];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (! [object isKindOfClass:[OCTNode class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OCTNode *another = object;
|
||||
|
||||
return [self compareString:self.ipv4Host with:another.ipv4Host] &&
|
||||
[self compareString:self.ipv6Host with:another.ipv6Host] &&
|
||||
(self.udpPort == another.udpPort) &&
|
||||
[self.tcpPorts isEqual:another.tcpPorts] &&
|
||||
[self.publicKey isEqual:another.publicKey];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
const NSUInteger prime = 31;
|
||||
NSUInteger result = 1;
|
||||
|
||||
result = prime * result + [self.ipv4Host hash];
|
||||
result = prime * result + [self.ipv6Host hash];
|
||||
result = prime * result + self.udpPort;
|
||||
result = prime * result + [self.tcpPorts hash];
|
||||
result = prime * result + [self.publicKey hash];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)compareString:(NSString *)first with:(NSString *)second
|
||||
{
|
||||
if (first && second) {
|
||||
return [first isEqual:second];
|
||||
}
|
||||
|
||||
BOOL bothNil = ! first && ! second;
|
||||
return bothNil;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,55 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTObject.h"
|
||||
|
||||
@implementation OCTObject
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (NSString *)primaryKey
|
||||
{
|
||||
return NSStringFromSelector(@selector(uniqueIdentifier));
|
||||
}
|
||||
|
||||
+ (NSDictionary *)defaultPropertyValues
|
||||
{
|
||||
return @{
|
||||
NSStringFromSelector(@selector(uniqueIdentifier)) : [[NSUUID UUID] UUIDString],
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSArray *)requiredProperties
|
||||
{
|
||||
return @[NSStringFromSelector(@selector(uniqueIdentifier))];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@ with uniqueIdentifier %@", [self class], self.uniqueIdentifier];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (object == self) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (! [object isKindOfClass:[self class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OCTObject *o = object;
|
||||
|
||||
return [self.uniqueIdentifier isEqualToString:o.uniqueIdentifier];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [self.uniqueIdentifier hash];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,22 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTObject.h"
|
||||
|
||||
@interface OCTSettingsStorageObject : OCTObject
|
||||
|
||||
@property BOOL bootstrapDidConnect;
|
||||
|
||||
/**
|
||||
* UIImage with avatar of user.
|
||||
*/
|
||||
@property NSData *userAvatarData;
|
||||
|
||||
/**
|
||||
* Generic data to be used by user of the library.
|
||||
* It shouldn't be used by objcTox itself.
|
||||
*/
|
||||
@property NSData *genericSettingsData;
|
||||
|
||||
@end
|
@ -0,0 +1,17 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSettingsStorageObject.h"
|
||||
|
||||
@implementation OCTSettingsStorageObject
|
||||
|
||||
+ (NSDictionary *)defaultPropertyValues
|
||||
{
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[super defaultPropertyValues]];
|
||||
|
||||
dict[@"bootstrapDidConnect"] = @NO;
|
||||
return [dict copy];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerBootstrap.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerBootstrapImpl : NSObject <OCTSubmanagerBootstrap, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
@ -0,0 +1,255 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerBootstrapImpl.h"
|
||||
#import "OCTNode.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTSettingsStorageObject.h"
|
||||
|
||||
static const NSTimeInterval kDidConnectDelay = 2.0; // in seconds
|
||||
static const NSTimeInterval kIterationTime = 5.0; // in seconds
|
||||
static const NSUInteger kNodesPerIteration = 20;
|
||||
|
||||
@interface OCTSubmanagerBootstrapImpl ()
|
||||
|
||||
@property (strong, nonatomic) NSMutableSet *addedNodes;
|
||||
|
||||
@property (assign, nonatomic) BOOL isBootstrapping;
|
||||
|
||||
@property (strong, nonatomic) NSObject *bootstrappingLock;
|
||||
|
||||
@property (assign, nonatomic) NSTimeInterval didConnectDelay;
|
||||
@property (assign, nonatomic) NSTimeInterval iterationTime;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTSubmanagerBootstrapImpl
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_addedNodes = [NSMutableSet new];
|
||||
_bootstrappingLock = [NSObject new];
|
||||
|
||||
_didConnectDelay = kDidConnectDelay;
|
||||
_iterationTime = kIterationTime;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)addNodeWithIpv4Host:(nullable NSString *)ipv4Host
|
||||
ipv6Host:(nullable NSString *)ipv6Host
|
||||
udpPort:(OCTToxPort)udpPort
|
||||
tcpPorts:(NSArray<NSNumber *> *)tcpPorts
|
||||
publicKey:(NSString *)publicKey
|
||||
{
|
||||
OCTNode *node = [[OCTNode alloc] initWithIpv4Host:ipv4Host
|
||||
ipv6Host:ipv6Host
|
||||
udpPort:udpPort
|
||||
tcpPorts:tcpPorts
|
||||
publicKey:publicKey];
|
||||
|
||||
@synchronized(self.addedNodes) {
|
||||
[self.addedNodes addObject:node];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addPredefinedNodes
|
||||
{
|
||||
NSString *file = [[self objcToxBundle] pathForResource:@"nodes" ofType:@"json"];
|
||||
NSData *data = [NSData dataWithContentsOfFile:file];
|
||||
|
||||
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
NSAssert(dictionary, @"Nodes json file is corrupted.");
|
||||
|
||||
for (NSDictionary *node in dictionary[@"nodes"]) {
|
||||
NSUInteger lastPing = [node[@"last_ping"] unsignedIntegerValue];
|
||||
|
||||
if (lastPing == 0) {
|
||||
// Skip nodes that weren't seen online.
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *ipv4 = node[@"ipv4"];
|
||||
NSString *ipv6 = node[@"ipv6"];
|
||||
OCTToxPort udpPort = [node[@"port"] unsignedShortValue];
|
||||
NSArray<NSNumber *> *tcpPorts = node[@"tcp_ports"];
|
||||
NSString *publicKey = node[@"public_key"];
|
||||
|
||||
// Check if addresses are valid.
|
||||
if (ipv4.length <= 2) {
|
||||
ipv4 = nil;
|
||||
}
|
||||
if (ipv6.length <= 2) {
|
||||
ipv6 = nil;
|
||||
}
|
||||
|
||||
NSAssert(ipv4, @"Nodes json file is corrupted");
|
||||
NSAssert(udpPort > 0, @"Nodes json file is corrupted");
|
||||
NSAssert(publicKey, @"Nodes json file is corrupted");
|
||||
|
||||
[self addNodeWithIpv4Host:ipv4 ipv6Host:ipv6 udpPort:udpPort tcpPorts:tcpPorts publicKey:publicKey];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)bootstrap
|
||||
{
|
||||
@synchronized(self.bootstrappingLock) {
|
||||
if (self.isBootstrapping) {
|
||||
OCTLogWarn(@"bootstrap method called while already bootstrapping");
|
||||
return;
|
||||
}
|
||||
self.isBootstrapping = YES;
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"bootstrapping with %lu nodes", (unsigned long)self.addedNodes.count);
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
if (realmManager.settingsStorage.bootstrapDidConnect) {
|
||||
OCTLogVerbose(@"did connect before, waiting %g seconds", self.didConnectDelay);
|
||||
[self tryToBootstrapAfter:self.didConnectDelay];
|
||||
}
|
||||
else {
|
||||
[self tryToBootstrap];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)tryToBootstrapAfter:(NSTimeInterval)after
|
||||
{
|
||||
__weak OCTSubmanagerBootstrapImpl *weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, after * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||
__strong OCTSubmanagerBootstrapImpl *strongSelf = weakSelf;
|
||||
|
||||
if (! strongSelf) {
|
||||
OCTLogInfo(@"OCTSubmanagerBootstrap is dead, seems that OCTManager was killed, quiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf tryToBootstrap];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)tryToBootstrap
|
||||
{
|
||||
if ([self.dataSource managerIsToxConnected]) {
|
||||
OCTLogInfo(@"trying to bootstrap... tox is connected, exiting");
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
[realmManager updateObject:realmManager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
|
||||
object.bootstrapDidConnect = YES;
|
||||
}];
|
||||
|
||||
[self finishBootstrapping];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *selectedNodes = [self selectedNodesForIteration];
|
||||
|
||||
if (! selectedNodes.count) {
|
||||
OCTLogInfo(@"trying to bootstrap... no nodes left, exiting");
|
||||
[self finishBootstrapping];
|
||||
return;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"trying to bootstrap... picked %lu nodes", (unsigned long)selectedNodes.count);
|
||||
|
||||
for (OCTNode *node in selectedNodes) {
|
||||
// HINT: do not do this async on a thread. since "node" will loose its value
|
||||
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
[self safeBootstrapFromHost:node.ipv4Host port:node.udpPort publicKey:node.publicKey];
|
||||
[self safeBootstrapFromHost:node.ipv6Host port:node.udpPort publicKey:node.publicKey];
|
||||
|
||||
for (NSNumber *tcpPort in node.tcpPorts) {
|
||||
[self safeAddTcpRelayWithHost:node.ipv4Host port:tcpPort.intValue publicKey:node.publicKey];
|
||||
[self safeAddTcpRelayWithHost:node.ipv6Host port:tcpPort.intValue publicKey:node.publicKey];
|
||||
}
|
||||
// });
|
||||
}
|
||||
|
||||
[self tryToBootstrapAfter:self.iterationTime];
|
||||
}
|
||||
|
||||
- (void)safeBootstrapFromHost:(NSString *)host port:(OCTToxPort)port publicKey:(NSString *)publicKey
|
||||
{
|
||||
if (! host) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
|
||||
if (! [[self.dataSource managerGetTox] bootstrapFromHost:host port:port publicKey:publicKey error:&error]) {
|
||||
OCTLogWarn(@"trying to bootstrap... bootstrap failed with address %@, error %@", host, error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)safeAddTcpRelayWithHost:(NSString *)host port:(OCTToxPort)port publicKey:(NSString *)publicKey
|
||||
{
|
||||
if (! host) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
|
||||
if (! [[self.dataSource managerGetTox] addTCPRelayWithHost:host port:port publicKey:publicKey error:&error]) {
|
||||
OCTLogWarn(@"trying to bootstrap... tcp relay failed with address %@, error %@", host, error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishBootstrapping
|
||||
{
|
||||
@synchronized(self.bootstrappingLock) {
|
||||
self.isBootstrapping = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)selectedNodesForIteration
|
||||
{
|
||||
NSMutableArray *allNodes;
|
||||
NSMutableArray *selectedNodes = [NSMutableArray new];
|
||||
|
||||
@synchronized(self.addedNodes) {
|
||||
allNodes = [[self.addedNodes allObjects] mutableCopy];
|
||||
}
|
||||
|
||||
while (allNodes.count && (selectedNodes.count < kNodesPerIteration)) {
|
||||
NSUInteger index = arc4random_uniform((u_int32_t)allNodes.count);
|
||||
|
||||
[selectedNodes addObject:allNodes[index]];
|
||||
[allNodes removeObjectAtIndex:index];
|
||||
}
|
||||
|
||||
@synchronized(self.addedNodes) {
|
||||
[self.addedNodes minusSet:[NSSet setWithArray:selectedNodes]];
|
||||
}
|
||||
|
||||
return [selectedNodes copy];
|
||||
}
|
||||
|
||||
- (NSBundle *)objcToxBundle
|
||||
{
|
||||
NSBundle *mainBundle = [NSBundle bundleForClass:[self class]];
|
||||
NSBundle *objcToxBundle = [NSBundle bundleWithPath:[mainBundle pathForResource:@"objcTox" ofType:@"bundle"]];
|
||||
|
||||
// objcToxBundle is used when installed with CocoaPods. If we run tests/demo app mainBundle would be used.
|
||||
return objcToxBundle ?: mainBundle;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,27 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerCalls.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
#import "OCTToxAV.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
#import "OCTAudioEngine.h"
|
||||
#import "OCTVideoEngine.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTCall+Utilities.h"
|
||||
#import "OCTCallTimer.h"
|
||||
|
||||
@class OCTTox;
|
||||
|
||||
@interface OCTSubmanagerCallsImpl : NSObject <OCTSubmanagerCalls, OCTSubmanagerProtocol>
|
||||
|
||||
/**
|
||||
* Initialize the OCTSubmanagerCall
|
||||
*/
|
||||
- (instancetype)initWithTox:(OCTTox *)tox;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
@ -0,0 +1,564 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerCallsImpl.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "OCTTox.h"
|
||||
|
||||
const OCTToxAVAudioBitRate kDefaultAudioBitRate = OCTToxAVAudioBitRate48;
|
||||
const OCTToxAVVideoBitRate kDefaultVideoBitRate = 2500;
|
||||
|
||||
@interface OCTSubmanagerCallsImpl () <OCTToxAVDelegate>
|
||||
|
||||
@property (strong, nonatomic) OCTToxAV *toxAV;
|
||||
@property (strong, nonatomic) OCTAudioEngine *audioEngine;
|
||||
@property (strong, nonatomic) OCTVideoEngine *videoEngine;
|
||||
@property (strong, nonatomic) OCTCallTimer *timer;
|
||||
@property (nonatomic, assign) dispatch_once_t setupOnceToken;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTSubmanagerCallsImpl : NSObject
|
||||
@synthesize delegate = _delegate;
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithTox:(OCTTox *)tox
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_toxAV = [[OCTToxAV alloc] initWithTox:tox error:nil];
|
||||
_toxAV.delegate = self;
|
||||
[_toxAV start];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self endAllCalls];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)setupAndReturnError:(NSError **)error
|
||||
{
|
||||
NSAssert(self.dataSource, @"dataSource is needed before setup of OCTSubmanagerCalls");
|
||||
__block BOOL status = NO;
|
||||
dispatch_once(&_setupOnceToken, ^{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
self.timer = [[OCTCallTimer alloc] initWithRealmManager:realmManager];
|
||||
|
||||
self.audioEngine = [OCTAudioEngine new];
|
||||
self.audioEngine.toxav = self.toxAV;
|
||||
self.videoEngine = [OCTVideoEngine new];
|
||||
self.videoEngine.toxav = self.toxAV;
|
||||
|
||||
status = [self.videoEngine setupAndReturnError:error];
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
- (OCTCall *)callToChat:(OCTChat *)chat enableAudio:(BOOL)enableAudio enableVideo:(BOOL)enableVideo error:(NSError **)error
|
||||
{
|
||||
OCTToxAVAudioBitRate audioBitRate = (enableAudio) ? kDefaultAudioBitRate : OCTToxAVAudioBitRateDisabled;
|
||||
OCTToxAVVideoBitRate videoBitRate = (enableVideo) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
|
||||
|
||||
|
||||
if (chat.friends.count == 1) {
|
||||
OCTFriend *friend = chat.friends.lastObject;
|
||||
self.audioEngine.friendNumber = friend.friendNumber;
|
||||
|
||||
if (! [self.toxAV callFriendNumber:friend.friendNumber
|
||||
audioBitRate:audioBitRate
|
||||
videoBitRate:videoBitRate
|
||||
error:error]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[self checkForCurrentActiveCallAndPause];
|
||||
|
||||
OCTCall *call = [self createCallWithFriend:friend status:OCTCallStatusDialing];
|
||||
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.status = OCTCallStatusDialing;
|
||||
callToUpdate.videoIsEnabled = enableVideo;
|
||||
}];
|
||||
|
||||
self.enableMicrophone = YES;
|
||||
|
||||
return call;
|
||||
}
|
||||
else {
|
||||
// TO DO: Group Calls
|
||||
return nil;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)enableVideoSending:(BOOL)enable forCall:(OCTCall *)call error:(NSError **)error
|
||||
{
|
||||
OCTToxAVVideoBitRate bitrate = (enable) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
|
||||
if (! [self setVideoBitrate:bitrate forCall:call error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (enable && (! [call isPaused])) {
|
||||
OCTFriend *friend = [call.chat.friends firstObject];
|
||||
self.videoEngine.friendNumber = friend.friendNumber;
|
||||
[self.videoEngine startSendingVideo];
|
||||
}
|
||||
else {
|
||||
[self.videoEngine stopSendingVideo];
|
||||
}
|
||||
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.videoIsEnabled = enable;
|
||||
}];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)answerCall:(OCTCall *)call enableAudio:(BOOL)enableAudio enableVideo:(BOOL)enableVideo error:(NSError **)error
|
||||
{
|
||||
OCTToxAVAudioBitRate audioBitRate = (enableAudio) ? kDefaultAudioBitRate : OCTToxAVAudioBitRateDisabled;
|
||||
OCTToxAVVideoBitRate videoBitRate = (enableVideo) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
|
||||
|
||||
if (call.chat.friends.count == 1) {
|
||||
|
||||
OCTFriend *friend = call.chat.friends.firstObject;
|
||||
|
||||
if (! [self.toxAV answerIncomingCallFromFriend:friend.friendNumber
|
||||
audioBitRate:audioBitRate
|
||||
videoBitRate:videoBitRate
|
||||
error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self checkForCurrentActiveCallAndPause];
|
||||
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
call.status = OCTCallStatusActive;
|
||||
callToUpdate.videoIsEnabled = enableVideo;
|
||||
}];
|
||||
|
||||
self.enableMicrophone = YES;
|
||||
[self startEnginesAndTimer:YES forCall:call];
|
||||
|
||||
return YES;
|
||||
}
|
||||
else {
|
||||
// TO DO: Group Calls
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)enableMicrophone
|
||||
{
|
||||
return self.audioEngine.enableMicrophone;
|
||||
}
|
||||
|
||||
- (void)setEnableMicrophone:(BOOL)enableMicrophone
|
||||
{
|
||||
self.audioEngine.enableMicrophone = enableMicrophone;
|
||||
}
|
||||
|
||||
- (BOOL)sendCallControl:(OCTToxAVCallControl)control toCall:(OCTCall *)call error:(NSError **)error
|
||||
{
|
||||
if (call.chat.friends.count == 1) {
|
||||
|
||||
OCTFriend *friend = call.chat.friends.firstObject;
|
||||
|
||||
if (! [self.toxAV sendCallControl:control toFriendNumber:friend.friendNumber error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
switch (control) {
|
||||
case OCTToxAVCallControlResume:
|
||||
[self checkForCurrentActiveCallAndPause];
|
||||
[self putOnPause:NO call:call];
|
||||
break;
|
||||
case OCTToxAVCallControlCancel:
|
||||
[self addMessageAndDeleteCall:call];
|
||||
|
||||
if ((self.audioEngine.friendNumber == friend.friendNumber) &&
|
||||
([self.audioEngine isAudioRunning:nil])) {
|
||||
[self startEnginesAndTimer:NO forCall:call];
|
||||
}
|
||||
|
||||
break;
|
||||
case OCTToxAVCallControlPause:
|
||||
[self putOnPause:YES call:call];
|
||||
break;
|
||||
case OCTToxAVCallControlUnmuteAudio:
|
||||
break;
|
||||
case OCTToxAVCallControlMuteAudio:
|
||||
break;
|
||||
case OCTToxAVCallControlHideVideo:
|
||||
break;
|
||||
case OCTToxAVCallControlShowVideo:
|
||||
break;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (OCTView *)videoFeed
|
||||
{
|
||||
return [self.videoEngine videoFeed];
|
||||
}
|
||||
|
||||
- (void)getVideoCallPreview:(void (^)(CALayer *))completionBlock
|
||||
{
|
||||
[self.videoEngine getVideoCallPreview:completionBlock];
|
||||
}
|
||||
|
||||
- (BOOL)setAudioBitrate:(int)bitrate forCall:(OCTCall *)call error:(NSError **)error
|
||||
{
|
||||
if (call.chat.friends.count == 1) {
|
||||
|
||||
OCTFriend *friend = call.chat.friends.firstObject;
|
||||
|
||||
return [self.toxAV setAudioBitRate:bitrate force:NO forFriend:friend.friendNumber error:error];
|
||||
}
|
||||
else {
|
||||
// TO DO: Group Calls
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Setting IO devices
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)setAudioInputDevice:(NSString *)deviceUniqueID error:(NSError **)error
|
||||
{
|
||||
return [self.audioEngine setInputDeviceID:deviceUniqueID error:error];
|
||||
}
|
||||
|
||||
- (BOOL)setAudioOutputDevice:(NSString *)deviceUniqueID error:(NSError **)error
|
||||
{
|
||||
return [self.audioEngine setOutputDeviceID:deviceUniqueID error:error];
|
||||
}
|
||||
|
||||
- (BOOL)setVideoInputDevice:(NSString *)deviceUniqueID error:(NSError **)error
|
||||
{
|
||||
return [self.videoEngine switchToCamera:deviceUniqueID error:error];
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error
|
||||
{
|
||||
return [self.audioEngine routeAudioToSpeaker:speaker error:error];
|
||||
}
|
||||
|
||||
- (BOOL)switchToCameraFront:(BOOL)front error:(NSError **)error
|
||||
{
|
||||
return [self.videoEngine useFrontCamera:front error:error];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark Private methods
|
||||
|
||||
- (OCTCall *)createCallWithFriend:(OCTFriend *)friend status:(OCTCallStatus)status
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
return [realmManager createCallWithChat:chat status:status];
|
||||
}
|
||||
|
||||
- (OCTCall *)getCurrentCallForFriendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
return [realmManager getCurrentCallForChat:chat];
|
||||
}
|
||||
|
||||
- (void)updateCall:(OCTCall *)call withStatus:(OCTCallStatus)status
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.status = status;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)endAllCalls
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[self startEnginesAndTimer:NO forCall:nil];
|
||||
|
||||
RLMResults *calls = [realmManager objectsWithClass:[OCTCall class] predicate:nil];
|
||||
|
||||
for (OCTCall *call in calls) {
|
||||
OCTFriend *friend = call.chat.friends.firstObject;
|
||||
[self.toxAV sendCallControl:OCTToxAVCallControlCancel toFriendNumber:friend.friendNumber error:nil];
|
||||
}
|
||||
|
||||
[realmManager convertAllCallsToMessages];
|
||||
}
|
||||
|
||||
- (void)putOnPause:(BOOL)pause call:(OCTCall *)call
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
BOOL wasPaused = call.pausedStatus != OCTCallPausedStatusNone;
|
||||
|
||||
if (pause) {
|
||||
if (! wasPaused) {
|
||||
[self startEnginesAndTimer:NO forCall:call];
|
||||
}
|
||||
}
|
||||
else {
|
||||
OCTFriend *friend = [call.chat.friends firstObject];
|
||||
self.audioEngine.friendNumber = friend.friendNumber;
|
||||
|
||||
if (call.pausedStatus == OCTCallPausedStatusByUser) {
|
||||
[self startEnginesAndTimer:YES forCall:call];
|
||||
}
|
||||
}
|
||||
|
||||
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
if (pause) {
|
||||
callToUpdate.pausedStatus |= OCTCallPausedStatusByUser;
|
||||
callToUpdate.onHoldStartInterval = callToUpdate.onHoldStartInterval ?: [[NSDate date] timeIntervalSince1970];
|
||||
}
|
||||
else {
|
||||
callToUpdate.pausedStatus &= ~OCTCallPausedStatusByUser;
|
||||
callToUpdate.onHoldStartInterval = 0;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addMessageAndDeleteCall:(OCTCall *)call
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
[realmManager addMessageCall:call];
|
||||
|
||||
if (! [call isPaused]) {
|
||||
[self.timer stopTimer];
|
||||
}
|
||||
|
||||
// TODO: this one crashes when video was active, and is called directly
|
||||
// there is a race condition somewhere -> fix me!
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||
[realmManager deleteObject:call];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateCall:(OCTCall *)call withState:(OCTToxAVCallState)state pausedStatus:(OCTCallPausedStatus)pausedStatus
|
||||
{
|
||||
BOOL sendingAudio = NO, sendingVideo = NO, acceptingAudio = NO, acceptingVideo = NO;
|
||||
|
||||
if (state & OCTToxAVFriendCallStateAcceptingAudio) {
|
||||
acceptingAudio = YES;
|
||||
}
|
||||
|
||||
if (state & OCTToxAVFriendCallStateAcceptingVideo) {
|
||||
acceptingVideo = YES;
|
||||
}
|
||||
|
||||
if (state & OCTToxAVFriendCallStateSendingAudio) {
|
||||
sendingAudio = YES;
|
||||
}
|
||||
|
||||
if (state & OCTToxAVFriendCallStateSendingVideo) {
|
||||
sendingVideo = YES;
|
||||
}
|
||||
|
||||
BOOL wasPaused = call.pausedStatus != OCTCallPausedStatusNone;
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.friendAcceptingAudio = acceptingAudio;
|
||||
callToUpdate.friendAcceptingVideo = acceptingVideo;
|
||||
callToUpdate.friendSendingAudio = sendingAudio;
|
||||
callToUpdate.friendSendingVideo = sendingVideo;
|
||||
callToUpdate.pausedStatus = pausedStatus;
|
||||
|
||||
if (! wasPaused && (state == OCTToxAVFriendCallStatePaused)) {
|
||||
callToUpdate.onHoldStartInterval = [[NSDate date] timeIntervalSince1970];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)checkForCurrentActiveCallAndPause
|
||||
{
|
||||
if ([self.audioEngine isAudioRunning:nil] || [self.videoEngine isSendingVideo]) {
|
||||
OCTCall *call = [self getCurrentCallForFriendNumber:self.audioEngine.friendNumber];
|
||||
[self sendCallControl:OCTToxAVCallControlPause toCall:call error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)setVideoBitrate:(int)bitrate forCall:(OCTCall *)call error:(NSError **)error
|
||||
{
|
||||
if (call.chat.friends.count == 1) {
|
||||
|
||||
OCTFriend *friend = call.chat.friends.firstObject;
|
||||
|
||||
return [self.toxAV setVideoBitRate:bitrate force:NO forFriend:friend.friendNumber error:error];
|
||||
}
|
||||
else {
|
||||
// TO DO: Group Calls
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startEnginesAndTimer:(BOOL)start forCall:(OCTCall *)call
|
||||
{
|
||||
if (start) {
|
||||
OCTFriend *friend = [call.chat.friends firstObject];
|
||||
|
||||
NSError *error;
|
||||
if (! [self.audioEngine startAudioFlow:&error]) {
|
||||
OCTLogVerbose(@"Error starting audio flow %@", error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (call.videoIsEnabled) {
|
||||
[self.videoEngine startSendingVideo];
|
||||
}
|
||||
|
||||
self.audioEngine.friendNumber = friend.friendNumber;
|
||||
self.videoEngine.friendNumber = friend.friendNumber;
|
||||
|
||||
[self.timer startTimerForCall:call];
|
||||
}
|
||||
else {
|
||||
[self.audioEngine stopAudioFlow:nil];
|
||||
[self.videoEngine stopSendingVideo];
|
||||
[self.timer stopTimer];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark OCTToxAV delegate methods
|
||||
|
||||
- (void)toxAV:(OCTToxAV *)toxAV receiveCallAudioEnabled:(BOOL)audio videoEnabled:(BOOL)video friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
OCTCall *call = [self createCallWithFriend:friend status:OCTCallStatusRinging];
|
||||
|
||||
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
|
||||
callToUpdate.status = OCTCallStatusRinging;
|
||||
callToUpdate.caller = friend;
|
||||
callToUpdate.friendSendingAudio = audio;
|
||||
callToUpdate.friendAcceptingAudio = audio;
|
||||
callToUpdate.friendSendingVideo = video;
|
||||
callToUpdate.friendAcceptingVideo = video;
|
||||
}];
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(callSubmanager:receiveCall:audioEnabled:videoEnabled:)]) {
|
||||
[self.delegate callSubmanager:self receiveCall:call audioEnabled:audio videoEnabled:video];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toxAV:(OCTToxAV *)toxAV callStateChanged:(OCTToxAVCallState)state friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTCall *call = [self getCurrentCallForFriendNumber:friendNumber];
|
||||
|
||||
if ((state & OCTToxAVFriendCallStateFinished) || (state & OCTToxAVFriendCallStateError)) {
|
||||
|
||||
[self addMessageAndDeleteCall:call];
|
||||
|
||||
if ((self.audioEngine.friendNumber == friendNumber) && [self.audioEngine isAudioRunning:nil]) {
|
||||
[self.audioEngine stopAudioFlow:nil];
|
||||
}
|
||||
|
||||
if ((self.videoEngine.friendNumber == friendNumber) && [self.videoEngine isSendingVideo]) {
|
||||
[self.videoEngine stopSendingVideo];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (call.status == OCTCallStatusDialing) {
|
||||
[self updateCall:call withStatus:OCTCallStatusActive];
|
||||
[self startEnginesAndTimer:YES forCall:call];
|
||||
}
|
||||
|
||||
OCTCallPausedStatus pauseStatus = call.pausedStatus;
|
||||
|
||||
if ((pauseStatus == OCTCallPausedStatusNone) && (state == OCTToxAVFriendCallStatePaused)) {
|
||||
[self startEnginesAndTimer:NO forCall:call];
|
||||
}
|
||||
|
||||
if ((pauseStatus == OCTCallPausedStatusByFriend) && (state != OCTToxAVFriendCallStatePaused)) {
|
||||
[self startEnginesAndTimer:YES forCall:call];
|
||||
}
|
||||
|
||||
if (state == OCTToxAVFriendCallStatePaused) {
|
||||
pauseStatus |= OCTCallPausedStatusByFriend;
|
||||
}
|
||||
else {
|
||||
pauseStatus &= ~OCTCallPausedStatusByFriend;
|
||||
}
|
||||
|
||||
[self updateCall:call withState:state pausedStatus:pauseStatus];
|
||||
}
|
||||
|
||||
- (void) toxAV:(OCTToxAV *)toxAV
|
||||
receiveAudio:(OCTToxAVPCMData *)pcm
|
||||
sampleCount:(OCTToxAVSampleCount)sampleCount
|
||||
channels:(OCTToxAVChannels)channels
|
||||
sampleRate:(OCTToxAVSampleRate)sampleRate
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
// TOXAUDIO: -incoming-audio-
|
||||
[self.audioEngine provideAudioFrames:pcm sampleCount:sampleCount channels:channels sampleRate:sampleRate fromFriend:friendNumber];
|
||||
}
|
||||
|
||||
- (void)toxAV:(OCTToxAV *)toxAV audioBitRateStatus:(OCTToxAVAudioBitRate)audioBitRate forFriendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
// TODO https://github.com/Antidote-for-Tox/objcTox/issues/88
|
||||
}
|
||||
|
||||
- (void)toxAV:(OCTToxAV *)toxAV videoBitRateStatus:(OCTToxAVVideoBitRate)audioBitRate forFriendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
// TODO https://github.com/Antidote-for-Tox/objcTox/issues/88
|
||||
}
|
||||
|
||||
- (void) toxAV:(OCTToxAV *)toxAV
|
||||
receiveVideoFrameWithWidth:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height
|
||||
yPlane:(OCTToxAVPlaneData *)yPlane uPlane:(OCTToxAVPlaneData *)uPlane
|
||||
vPlane:(OCTToxAVPlaneData *)vPlane
|
||||
yStride:(OCTToxAVStrideData)yStride uStride:(OCTToxAVStrideData)uStride
|
||||
vStride:(OCTToxAVStrideData)vStride
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
[self.videoEngine receiveVideoFrameWithWidth:width
|
||||
height:height
|
||||
yPlane:yPlane
|
||||
uPlane:uPlane
|
||||
vPlane:vPlane
|
||||
yStride:yStride
|
||||
uStride:uStride
|
||||
vStride:vStride
|
||||
friendNumber:friendNumber];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerChats.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerChatsImpl : NSObject <OCTSubmanagerChats, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
@ -0,0 +1,529 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerChatsImpl.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTMessageAbstract.h"
|
||||
#import "OCTMessageText.h"
|
||||
#import "OCTChat.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "OCTSendMessageOperation.h"
|
||||
#import "OCTTox+Private.h"
|
||||
#import "OCTToxOptions+Private.h"
|
||||
#import "Firebase.h"
|
||||
|
||||
@interface OCTSubmanagerChatsImpl ()
|
||||
|
||||
@property (strong, nonatomic, readonly) NSOperationQueue *sendMessageQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTSubmanagerChatsImpl
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_sendMessageQueue = [NSOperationQueue new];
|
||||
_sendMessageQueue.maxConcurrentOperationCount = 1;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self.dataSource.managerGetNotificationCenter removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)configure
|
||||
{
|
||||
[self.dataSource.managerGetNotificationCenter addObserver:self
|
||||
selector:@selector(friendConnectionStatusChangeNotification:)
|
||||
name:kOCTFriendConnectionStatusChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (OCTChat *)getOrCreateChatWithFriend:(OCTFriend *)friend
|
||||
{
|
||||
return [[self.dataSource managerGetRealmManager] getOrCreateChatWithFriend:friend];
|
||||
}
|
||||
|
||||
- (void)removeMessages:(NSArray<OCTMessageAbstract *> *)messages
|
||||
{
|
||||
[[self.dataSource managerGetRealmManager] removeMessages:messages];
|
||||
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTScheduleFileTransferCleanupNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)removeAllMessagesInChat:(OCTChat *)chat removeChat:(BOOL)removeChat
|
||||
{
|
||||
[[self.dataSource managerGetRealmManager] removeAllMessagesInChat:chat removeChat:removeChat];
|
||||
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTScheduleFileTransferCleanupNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)sendOwnPush
|
||||
{
|
||||
NSLog(@"PUSH:sendOwnPush");
|
||||
NSString *token = [FIRMessaging messaging].FCMToken;
|
||||
if (token.length > 0)
|
||||
{
|
||||
NSString *my_pushToken = [NSString stringWithFormat:@"https://tox.zoff.xyz/toxfcm/fcm.php?id=%@&type=1", token];
|
||||
// NSLog(@"token push url=%@", my_pushToken);
|
||||
triggerPush(my_pushToken, nil, nil, nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"PUSH:sendOwnPush:no token");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sendMessagePushToChat:(OCTChat *)chat
|
||||
{
|
||||
NSParameterAssert(chat);
|
||||
NSLog(@"PUSH:sendMessagePushToChat");
|
||||
__weak OCTSubmanagerChatsImpl *weakSelf = self;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
|
||||
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
|
||||
|
||||
OCTFriend *friend = [chat.friends firstObject];
|
||||
__block NSString *friend_pushToken = friend.pushToken;
|
||||
|
||||
if (friend_pushToken == nil)
|
||||
{
|
||||
NSLog(@"sendMessagePushToChat:Friend has No Pushtoken");
|
||||
}
|
||||
else
|
||||
{
|
||||
// HINT: only select outgoing messages (senderUniqueIdentifier == NULL)
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.isDelivered == 0 AND messageText.sentPush == 0 AND senderUniqueIdentifier == nil", chat.uniqueIdentifier];
|
||||
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
OCTMessageAbstract *message_found = [results firstObject];
|
||||
if (message_found) {
|
||||
|
||||
[realmManager updateObject:message_found withBlock:^(OCTMessageAbstract *theMessage) {
|
||||
theMessage.messageText.sentPush = YES;
|
||||
}];
|
||||
triggerPush(friend_pushToken, message_found.messageText.msgv3HashHex, strongSelf, chat);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
triggerPush(NSString *used_pushToken,
|
||||
NSString *msgv3HashHex,
|
||||
OCTSubmanagerChatsImpl *strongSelf,
|
||||
OCTChat *chat)
|
||||
{
|
||||
// HINT: call push token (URL) here
|
||||
// best in a background thread
|
||||
//
|
||||
NSLog(@"PUSH:triggerPush");
|
||||
if ((used_pushToken != nil) && (used_pushToken.length > 5)) {
|
||||
|
||||
// check push url starts with allowed values
|
||||
if (
|
||||
([used_pushToken hasPrefix:@"https://tox.zoff.xyz/toxfcm/fcm.php?id="])
|
||||
||
|
||||
([used_pushToken hasPrefix:@"https://gotify1.unifiedpush.org/UP?token="])
|
||||
||
|
||||
([used_pushToken hasPrefix:@"https://ntfy.sh/"])
|
||||
) {
|
||||
|
||||
NSString *strong_pushToken = used_pushToken;
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// NSString *strong_pushToken = weak_pushToken;
|
||||
int PUSH_URL_TRIGGER_AGAIN_MAX_COUNT = 8;
|
||||
int PUSH_URL_TRIGGER_AGAIN_SECONDS = 21;
|
||||
|
||||
for (int i=0; i<(PUSH_URL_TRIGGER_AGAIN_MAX_COUNT + 1); i++)
|
||||
{
|
||||
if (chat == nil) {
|
||||
__block UIApplicationState as = UIApplicationStateBackground;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
as =[[UIApplication sharedApplication] applicationState];
|
||||
});
|
||||
|
||||
if (as == UIApplicationStateActive) {
|
||||
NSLog(@"PUSH:fg->break:1");
|
||||
break;
|
||||
}
|
||||
}
|
||||
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:strong_pushToken]];
|
||||
NSString *userUpdate = [NSString stringWithFormat:@"&text=1", nil];
|
||||
[urlRequest setHTTPMethod:@"POST"];
|
||||
|
||||
NSData *data1 = [userUpdate dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
[urlRequest setHTTPBody:data1];
|
||||
[urlRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData];
|
||||
[urlRequest setTimeoutInterval:10]; // HINT: 10 seconds
|
||||
NSString *userAgent = @"Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0";
|
||||
[urlRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
|
||||
[urlRequest setValue:@"no-cache, no-store, must-revalidate" forHTTPHeaderField:@"Cache-Control"];
|
||||
[urlRequest setValue:@"no-cache" forHTTPHeaderField:@"Pragma"];
|
||||
[urlRequest setValue:@"0" forHTTPHeaderField:@"Expires"];
|
||||
|
||||
// NSLog(@"PUSH:for msgv3HashHex=%@", msgv3HashHex);
|
||||
// NSLog(@"PUSH:for friend.pushToken=%@", strong_pushToken);
|
||||
|
||||
NSURLSession *session = [NSURLSession sharedSession];
|
||||
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
if ((httpResponse.statusCode < 300) && (httpResponse.statusCode > 199)) {
|
||||
NSLog(@"calling PUSH URL:CALL:SUCCESS");
|
||||
}
|
||||
else {
|
||||
NSLog(@"calling PUSH URL:-ERROR:01-");
|
||||
}
|
||||
}];
|
||||
NSLog(@"calling PUSH URL:CALL:start");
|
||||
[dataTask resume];
|
||||
if (i < PUSH_URL_TRIGGER_AGAIN_MAX_COUNT)
|
||||
{
|
||||
NSLog(@"calling PUSH URL:WAIT:start");
|
||||
[NSThread sleepForTimeInterval:PUSH_URL_TRIGGER_AGAIN_SECONDS];
|
||||
NSLog(@"calling PUSH URL:WAIT:done");
|
||||
}
|
||||
|
||||
if (chat == nil) {
|
||||
__block UIApplicationState as = UIApplicationStateBackground;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
as =[[UIApplication sharedApplication] applicationState];
|
||||
});
|
||||
|
||||
if (as == UIApplicationStateActive) {
|
||||
NSLog(@"PUSH:fg->break:2");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgv3HashHex != nil)
|
||||
{
|
||||
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
|
||||
__block BOOL msgIsDelivered = NO;
|
||||
|
||||
NSLog(@"calling PUSH URL:DB check:start");
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
// HINT: only select outgoing messages (senderUniqueIdentifier == NULL)
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier == nil",
|
||||
chat.uniqueIdentifier, msgv3HashHex];
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
OCTMessageAbstract *message_found = [results firstObject];
|
||||
if (message_found) {
|
||||
if (message_found.messageText) {
|
||||
msgIsDelivered = message_found.messageText.isDelivered;
|
||||
}
|
||||
}
|
||||
NSLog(@"calling PUSH URL:DB check:end_real");
|
||||
});
|
||||
NSLog(@"calling PUSH URL:DB check:end");
|
||||
|
||||
if (msgIsDelivered == YES) {
|
||||
// OCTLogInfo(@"PUSH:for msgv3HashHex isDelivered=YES");
|
||||
NSLog(@"PUSH:for msgv3HashHex isDelivered=YES");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
NSLog(@"unsafe PUSH URL not allowed:-ERROR:02-");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sendMessageToChat:(OCTChat *)chat
|
||||
text:(NSString *)text
|
||||
type:(OCTToxMessageType)type
|
||||
successBlock:(void (^)(OCTMessageAbstract *message))userSuccessBlock
|
||||
failureBlock:(void (^)(NSError *error))userFailureBlock
|
||||
{
|
||||
NSParameterAssert(chat);
|
||||
NSParameterAssert(text);
|
||||
|
||||
OCTFriend *friend = [chat.friends firstObject];
|
||||
|
||||
uint8_t *message_v3_hash_bin = calloc(1, TOX_MSGV3_MSGID_LENGTH);
|
||||
uint8_t *message_v3_hash_hexstr = calloc(1, (TOX_MSGV3_MSGID_LENGTH * 2) + 1);
|
||||
|
||||
NSString *msgv3HashHex = nil;
|
||||
UInt32 msgv3tssec = 0;
|
||||
|
||||
if ((message_v3_hash_bin) && (message_v3_hash_hexstr))
|
||||
{
|
||||
tox_messagev3_get_new_message_id(message_v3_hash_bin);
|
||||
bin_to_hex((const char *)message_v3_hash_bin, (size_t)TOX_MSGV3_MSGID_LENGTH, message_v3_hash_hexstr);
|
||||
|
||||
msgv3HashHex = [[NSString alloc] initWithBytes:message_v3_hash_hexstr length:(TOX_MSGV3_MSGID_LENGTH * 2) encoding:NSUTF8StringEncoding];
|
||||
|
||||
// HINT: set sent timestamp to now() as unixtimestamp value
|
||||
msgv3tssec = [[NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970]] integerValue];
|
||||
|
||||
free(message_v3_hash_bin);
|
||||
free(message_v3_hash_hexstr);
|
||||
}
|
||||
|
||||
__weak OCTSubmanagerChatsImpl *weakSelf = self;
|
||||
OCTSendMessageOperationSuccessBlock successBlock = ^(OCTToxMessageId messageId) {
|
||||
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
|
||||
|
||||
BOOL sent_push = NO;
|
||||
|
||||
if (messageId == -1) {
|
||||
if ((friend.pushToken != nil) && (friend.pushToken.length > 5)) {
|
||||
|
||||
// check push url starts with allowed values
|
||||
if (
|
||||
([friend.pushToken hasPrefix:@"https://tox.zoff.xyz/toxfcm/fcm.php?id="])
|
||||
||
|
||||
([friend.pushToken hasPrefix:@"https://gotify1.unifiedpush.org/UP?token="])
|
||||
||
|
||||
([friend.pushToken hasPrefix:@"https://ntfy.sh/"])
|
||||
) {
|
||||
sent_push = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
|
||||
OCTMessageAbstract *message = [realmManager addMessageWithText:text type:type chat:chat sender:nil messageId:messageId msgv3HashHex:msgv3HashHex sentPush:sent_push tssent:msgv3tssec tsrcvd:0];
|
||||
|
||||
if (userSuccessBlock) {
|
||||
userSuccessBlock(message);
|
||||
}
|
||||
};
|
||||
|
||||
OCTSendMessageOperationFailureBlock failureBlock = ^(NSError *error) {
|
||||
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
|
||||
|
||||
if ((error.code == OCTToxErrorFriendSendMessageFriendNotConnected) &&
|
||||
[strongSelf.dataSource managerUseFauxOfflineMessaging]) {
|
||||
NSString *friend_pushToken = friend.pushToken;
|
||||
triggerPush(friend_pushToken, msgv3HashHex, strongSelf, chat);
|
||||
successBlock(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userFailureBlock) {
|
||||
userFailureBlock(error);
|
||||
}
|
||||
};
|
||||
|
||||
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
|
||||
friendNumber:friend.friendNumber
|
||||
messageType:type
|
||||
message:text
|
||||
msgv3HashHex:msgv3HashHex
|
||||
msgv3tssec:msgv3tssec
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
[self.sendMessageQueue addOperation:operation];
|
||||
}
|
||||
|
||||
- (BOOL)setIsTyping:(BOOL)isTyping inChat:(OCTChat *)chat error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(chat);
|
||||
|
||||
OCTFriend *friend = [chat.friends firstObject];
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
|
||||
return [tox setUserIsTyping:isTyping forFriendNumber:friend.friendNumber error:error];
|
||||
}
|
||||
|
||||
#pragma mark - NSNotification
|
||||
|
||||
- (void)friendConnectionStatusChangeNotification:(NSNotification *)notification
|
||||
{
|
||||
OCTFriend *friend = notification.object;
|
||||
|
||||
if (! friend) {
|
||||
OCTLogWarn(@"no friend received in notification %@, exiting", notification);
|
||||
return;
|
||||
}
|
||||
|
||||
if (friend.isConnected) {
|
||||
[self resendUndeliveredMessagesToFriend:friend];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)resendUndeliveredMessagesToFriend:(OCTFriend *)friend
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@"
|
||||
@" AND senderUniqueIdentifier == nil"
|
||||
@" AND messageText.isDelivered == NO",
|
||||
chat.uniqueIdentifier];
|
||||
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
|
||||
for (OCTMessageAbstract *message in results) {
|
||||
OCTLogInfo(@"Resending message to friend %@", friend);
|
||||
|
||||
__weak OCTSubmanagerChatsImpl *weakSelf = self;
|
||||
OCTSendMessageOperationSuccessBlock successBlock = ^(OCTToxMessageId messageId) {
|
||||
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
|
||||
|
||||
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
|
||||
|
||||
[realmManager updateObject:message withBlock:^(OCTMessageAbstract *theMessage) {
|
||||
theMessage.messageText.messageId = messageId;
|
||||
}];
|
||||
};
|
||||
|
||||
OCTSendMessageOperationFailureBlock failureBlock = ^(NSError *error) {
|
||||
OCTLogWarn(@"Cannot resend message to friend %@, error %@", friend, error);
|
||||
};
|
||||
|
||||
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
|
||||
friendNumber:friend.friendNumber
|
||||
messageType:message.messageText.type
|
||||
message:message.messageText.text
|
||||
msgv3HashHex:message.messageText.msgv3HashHex
|
||||
msgv3tssec:message.tssent
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
[self.sendMessageQueue addOperation:operation];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - OCTToxDelegate
|
||||
|
||||
/*
|
||||
* send mesgV3 high level ACK message.
|
||||
*/
|
||||
- (void)tox:(OCTTox *)tox sendFriendHighlevelACK:(NSString *)message
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
sendTimestamp:(uint32_t)sendTimestamp
|
||||
{
|
||||
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
|
||||
friendNumber:friendNumber
|
||||
messageType:OCTToxMessageTypeHighlevelack
|
||||
message:message
|
||||
msgv3HashHex:msgv3HashHex
|
||||
msgv3tssec:sendTimestamp
|
||||
successBlock:nil
|
||||
failureBlock:nil];
|
||||
[self.sendMessageQueue addOperation:operation];
|
||||
}
|
||||
|
||||
/*
|
||||
* Process incoming text message from friend.
|
||||
*/
|
||||
- (void)tox:(OCTTox *)tox friendMessage:(NSString *)message
|
||||
type:(OCTToxMessageType)type
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
sendTimestamp:(uint32_t)sendTimestamp
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
if (msgv3HashHex != nil)
|
||||
{
|
||||
// HINT: check for double message, but only select incoming messages (senderUniqueIdentifier != NULL)
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier != nil",
|
||||
chat.uniqueIdentifier, msgv3HashHex];
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
OCTMessageAbstract *message_found = [results firstObject];
|
||||
|
||||
if (message_found) {
|
||||
OCTLogInfo(@"friendMessage ignoring double message i %@", chat.uniqueIdentifier);
|
||||
OCTLogInfo(@"friendMessage ignoring double message f %@", friend);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[realmManager addMessageWithText:message type:type chat:chat sender:friend messageId:0 msgv3HashHex:msgv3HashHex sentPush:NO tssent:sendTimestamp tsrcvd:0];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendHighLevelACK:(NSString *)message
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
msgv3HashHex:(NSString *)msgv3HashHex
|
||||
sendTimestamp:(uint32_t)sendTimestamp
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
// HINT: only select outgoing messages
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier == nil",
|
||||
chat.uniqueIdentifier, msgv3HashHex];
|
||||
|
||||
// HINT: we still sort and use only 1 result row, just in case more than 1 row is returned.
|
||||
// but if more than 1 row is returned that would actually be an error.
|
||||
// we use the newest message with this Hash
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
results = [results sortedResultsUsingKeyPath:@"dateInterval" ascending:YES];
|
||||
|
||||
OCTMessageAbstract *message_found = [results firstObject];
|
||||
|
||||
if (! message_found) {
|
||||
return;
|
||||
}
|
||||
|
||||
OCTLogInfo(@"friendHighLevelACK recevied from friend %@", friend);
|
||||
|
||||
[realmManager updateObject:message_found withBlock:^(OCTMessageAbstract *theMessage) {
|
||||
theMessage.messageText.isDelivered = YES;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (void)tox:(OCTTox *)tox messageDelivered:(OCTToxMessageId)messageId friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
if (friend.msgv3Capability == YES)
|
||||
{
|
||||
// HINT: if friend has msgV3 capability, we ignore the low level ACK and keep waiting for the high level ACK
|
||||
OCTLogInfo(@"messageDelivered ignoring low level ACK %@", friend);
|
||||
return;
|
||||
}
|
||||
|
||||
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.messageId == %d",
|
||||
chat.uniqueIdentifier, messageId];
|
||||
|
||||
// messageId is reset on every launch, so we want to update delivered status on latest message.
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
|
||||
results = [results sortedResultsUsingKeyPath:@"dateInterval" ascending:NO];
|
||||
|
||||
OCTMessageAbstract *message = [results firstObject];
|
||||
|
||||
if (! message) {
|
||||
return;
|
||||
}
|
||||
|
||||
[realmManager updateObject:message withBlock:^(OCTMessageAbstract *theMessage) {
|
||||
theMessage.messageText.isDelivered = YES;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,46 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class OCTTox;
|
||||
@class OCTRealmManager;
|
||||
@protocol OCTFileStorageProtocol;
|
||||
|
||||
/**
|
||||
* Notification is send when connection status of friend has changed.
|
||||
*
|
||||
* - object OCTFriend whose status has changed.
|
||||
* - userInfo nil
|
||||
*/
|
||||
static NSString *const kOCTFriendConnectionStatusChangeNotification = @"kOCTFriendConnectionStatusChangeNotification";
|
||||
|
||||
/**
|
||||
* Notification is send on user avatar update.
|
||||
*
|
||||
* - object nil
|
||||
* - userInfo nil
|
||||
*/
|
||||
static NSString *const kOCTUserAvatarWasUpdatedNotification = @"kOCTUserAvatarWasUpdatedNotification";
|
||||
|
||||
/**
|
||||
* Send this notifications to schedule cleanup of uploaded/downloaded files. All files without OCTMessageFile
|
||||
* will be removed.
|
||||
*
|
||||
* - object nil
|
||||
* - userInfo nil
|
||||
*/
|
||||
static NSString *const kOCTScheduleFileTransferCleanupNotification = @"kOCTScheduleFileTransferCleanupNotification";
|
||||
|
||||
@protocol OCTSubmanagerDataSource <NSObject>
|
||||
|
||||
- (OCTTox *)managerGetTox;
|
||||
- (BOOL)managerIsToxConnected;
|
||||
- (void)managerSaveTox;
|
||||
- (OCTRealmManager *)managerGetRealmManager;
|
||||
- (id<OCTFileStorageProtocol>)managerGetFileStorage;
|
||||
- (NSNotificationCenter *)managerGetNotificationCenter;
|
||||
- (BOOL)managerUseFauxOfflineMessaging;
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerFiles.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerFilesImpl : NSObject <OCTSubmanagerFiles, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerFriends.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerFriendsImpl : NSObject <OCTSubmanagerFriends, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
@ -0,0 +1,361 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerFriendsImpl.h"
|
||||
#import "OCTLogging.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTFriend.h"
|
||||
#import "OCTFriendRequest.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "Firebase.h"
|
||||
|
||||
@implementation OCTSubmanagerFriendsImpl
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)sendFriendRequestToAddress:(NSString *)address message:(NSString *)message error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(address);
|
||||
NSParameterAssert(message);
|
||||
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
|
||||
OCTToxFriendNumber friendNumber = [tox addFriendWithAddress:address message:message error:error];
|
||||
|
||||
if (friendNumber == kOCTToxFriendNumberFailure) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
return [self createFriendWithFriendNumber:friendNumber error:error];
|
||||
}
|
||||
|
||||
- (BOOL)approveFriendRequest:(OCTFriendRequest *)friendRequest error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(friendRequest);
|
||||
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
|
||||
OCTToxFriendNumber friendNumber = [tox addFriendWithNoRequestWithPublicKey:friendRequest.publicKey error:error];
|
||||
|
||||
if (friendNumber == kOCTToxFriendNumberFailure) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
[[self.dataSource managerGetRealmManager] deleteObject:friendRequest];
|
||||
|
||||
return [self createFriendWithFriendNumber:friendNumber error:error];
|
||||
}
|
||||
|
||||
- (void)removeFriendRequest:(OCTFriendRequest *)friendRequest
|
||||
{
|
||||
NSParameterAssert(friendRequest);
|
||||
|
||||
[[self.dataSource managerGetRealmManager] deleteObject:friendRequest];
|
||||
}
|
||||
|
||||
- (BOOL)removeFriend:(OCTFriend *)friend error:(NSError **)error
|
||||
{
|
||||
NSParameterAssert(friend);
|
||||
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
|
||||
if (! [tox deleteFriendWithFriendNumber:friend.friendNumber error:error]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
[[self.dataSource managerGetRealmManager] deleteObject:friend];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Private category
|
||||
|
||||
- (void)configure
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
|
||||
[realmManager updateObjectsWithClass:[OCTFriend class] predicate:nil updateBlock:^(OCTFriend *friend) {
|
||||
// Tox may change friendNumber after relaunch, resetting them.
|
||||
friend.friendNumber = kOCTToxFriendNumberFailure;
|
||||
}];
|
||||
|
||||
for (NSNumber *friendNumber in [tox friendsArray]) {
|
||||
OCTToxFriendNumber number = [friendNumber intValue];
|
||||
NSError *error;
|
||||
|
||||
NSString *publicKey = [tox publicKeyFromFriendNumber:number error:&error];
|
||||
|
||||
if (! publicKey) {
|
||||
@throw [NSException exceptionWithName:@"Cannot find publicKey for existing friendNumber, Tox save data is broken"
|
||||
reason:error.debugDescription
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"publicKey == %@", publicKey];
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
|
||||
|
||||
if (results.count == 0) {
|
||||
// It seems that friend is in Tox but isn't in Realm. Let's add it.
|
||||
[self createFriendWithFriendNumber:number error:nil];
|
||||
continue;
|
||||
}
|
||||
|
||||
OCTFriend *friend = [results firstObject];
|
||||
|
||||
// Reset some fields for friends.
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.friendNumber = number;
|
||||
theFriend.status = OCTToxUserStatusNone;
|
||||
theFriend.isConnected = NO;
|
||||
theFriend.connectionStatus = OCTToxConnectionStatusNone;
|
||||
theFriend.isTyping = NO;
|
||||
NSDate *dateOffline = [tox friendGetLastOnlineWithFriendNumber:number error:nil];
|
||||
theFriend.lastSeenOnlineInterval = [dateOffline timeIntervalSince1970];
|
||||
}];
|
||||
}
|
||||
|
||||
// Remove all OCTFriend's which aren't bounded to tox. User cannot interact with them anyway.
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"friendNumber == %d", kOCTToxFriendNumberFailure];
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
|
||||
|
||||
for (OCTFriend *friend in results) {
|
||||
[realmManager deleteObject:friend];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - OCTToxDelegate
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendRequestWithMessage:(NSString *)message publicKey:(NSString *)publicKey
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"publicKey == %@", publicKey];
|
||||
RLMResults *results = [realmManager objectsWithClass:[OCTFriendRequest class] predicate:predicate];
|
||||
if (results.count > 0) {
|
||||
// friendRequest already exists
|
||||
return;
|
||||
}
|
||||
|
||||
results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
|
||||
if (results.count > 0) {
|
||||
// friend with such publicKey already exists
|
||||
return;
|
||||
}
|
||||
|
||||
OCTFriendRequest *request = [OCTFriendRequest new];
|
||||
request.publicKey = publicKey;
|
||||
request.message = message;
|
||||
request.dateInterval = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
[realmManager addObject:request];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendNameUpdate:(NSString *)name friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.name = name;
|
||||
|
||||
if (name.length && [theFriend.nickname isEqualToString:theFriend.publicKey]) {
|
||||
theFriend.nickname = name;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendPushTokenUpdate:(NSString *)pushToken friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.pushToken = pushToken;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendStatusMessageUpdate:(NSString *)statusMessage friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.statusMessage = statusMessage;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendStatusUpdate:(OCTToxUserStatus)status friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.status = status;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendIsTypingUpdate:(BOOL)isTyping friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.isTyping = isTyping;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendSetMsgv3Capability:(BOOL)msgv3Capability friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
if (friend.msgv3Capability != msgv3Capability)
|
||||
{
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
theFriend.msgv3Capability = msgv3Capability;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tox:(OCTTox *)tox friendConnectionStatusChanged:(OCTToxConnectionStatus)status friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
[self.dataSource managerSaveTox];
|
||||
|
||||
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
|
||||
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
|
||||
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
|
||||
|
||||
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
|
||||
if ((status != OCTToxConnectionStatusNone)
|
||||
&& (theFriend.connectionStatus == OCTToxConnectionStatusNone))
|
||||
{
|
||||
// Friend is coming online now
|
||||
|
||||
OCTToxCapabilities f_caps = [tox friendGetCapabilitiesWithFriendNumber:friendNumber];
|
||||
OCTLogVerbose(@"f_caps=%lu", f_caps);
|
||||
NSString* cap_string = [NSString stringWithFormat:@"%lu", f_caps];
|
||||
theFriend.capabilities2 = cap_string;
|
||||
|
||||
NSString *token = [FIRMessaging messaging].FCMToken;
|
||||
if (token.length > 0)
|
||||
{
|
||||
// HINT: prepend a dummy "A" char as placeholder for Tox Packet ID.
|
||||
// it will be replaced in sendLosslessPacketWithFriendNumber by pktid
|
||||
NSString *data = [NSString stringWithFormat:@"Ahttps://tox.zoff.xyz/toxfcm/fcm.php?id=%@&type=1", token];
|
||||
// NSLog(@"token push url=%@", data);
|
||||
NSError *error;
|
||||
|
||||
// HINT: pktid 181 is for sending push urls to friends
|
||||
BOOL result = [tox sendLosslessPacketWithFriendNumber:friendNumber
|
||||
pktid:181
|
||||
data:data
|
||||
error:&error];
|
||||
}
|
||||
}
|
||||
theFriend.isConnected = (status != OCTToxConnectionStatusNone);
|
||||
theFriend.connectionStatus = status;
|
||||
|
||||
if (! theFriend.isConnected) {
|
||||
// Friend is offline now
|
||||
NSDate *dateOffline = [tox friendGetLastOnlineWithFriendNumber:friendNumber error:nil];
|
||||
NSTimeInterval timeSince = [dateOffline timeIntervalSince1970];
|
||||
theFriend.lastSeenOnlineInterval = timeSince;
|
||||
}
|
||||
}];
|
||||
|
||||
[[self.dataSource managerGetNotificationCenter] postNotificationName:kOCTFriendConnectionStatusChangeNotification object:friend];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (BOOL)createFriendWithFriendNumber:(OCTToxFriendNumber)friendNumber error:(NSError **)userError
|
||||
{
|
||||
OCTTox *tox = [self.dataSource managerGetTox];
|
||||
NSError *error;
|
||||
|
||||
OCTFriend *friend = [OCTFriend new];
|
||||
|
||||
friend.friendNumber = friendNumber;
|
||||
|
||||
friend.publicKey = [tox publicKeyFromFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.name = [tox friendNameWithFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.statusMessage = [tox friendStatusMessageWithFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.status = [tox friendStatusWithFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.connectionStatus = [tox friendConnectionStatusWithFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDate *lastSeenOnline = [tox friendGetLastOnlineWithFriendNumber:friendNumber error:&error];
|
||||
friend.lastSeenOnlineInterval = [lastSeenOnline timeIntervalSince1970];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.isTyping = [tox isFriendTypingWithFriendNumber:friendNumber error:&error];
|
||||
if ([self checkForError:error andAssignTo:userError]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
friend.isConnected = (friend.connectionStatus != OCTToxConnectionStatusNone);
|
||||
friend.nickname = friend.name.length ? friend.name : friend.publicKey;
|
||||
|
||||
[[self.dataSource managerGetRealmManager] addObject:friend];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)checkForError:(NSError *)toCheck andAssignTo:(NSError **)toAssign
|
||||
{
|
||||
if (! toCheck) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (toAssign) {
|
||||
*toAssign = toCheck;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerObjects.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerObjectsImpl : NSObject <OCTSubmanagerObjects, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
@ -0,0 +1,103 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerObjectsImpl.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTFriend.h"
|
||||
#import "OCTFriendRequest.h"
|
||||
#import "OCTChat.h"
|
||||
#import "OCTCall.h"
|
||||
#import "OCTMessageAbstract.h"
|
||||
#import "OCTSettingsStorageObject.h"
|
||||
|
||||
@implementation OCTSubmanagerObjectsImpl
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setGenericSettingsData:(NSData *)data
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[manager updateObject:manager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
|
||||
object.genericSettingsData = data;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSData *)genericSettingsData
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
return manager.settingsStorage.genericSettingsData;
|
||||
}
|
||||
|
||||
- (RLMResults *)objectsForType:(OCTFetchRequestType)type predicate:(NSPredicate *)predicate
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
return [manager objectsWithClass:[self classForFetchRequestType:type] predicate:predicate];
|
||||
}
|
||||
|
||||
- (OCTObject *)objectWithUniqueIdentifier:(NSString *)uniqueIdentifier forType:(OCTFetchRequestType)type
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
return [manager objectWithUniqueIdentifier:uniqueIdentifier class:[self classForFetchRequestType:type]];
|
||||
}
|
||||
|
||||
#pragma mark - Friends
|
||||
|
||||
- (void)changeFriend:(OCTFriend *)friend nickname:(NSString *)nickname
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[manager updateObject:friend withBlock:^(OCTFriend *theFriend) {
|
||||
if (nickname.length) {
|
||||
theFriend.nickname = nickname;
|
||||
}
|
||||
else if (theFriend.name.length) {
|
||||
theFriend.nickname = theFriend.name;
|
||||
}
|
||||
else {
|
||||
theFriend.nickname = theFriend.publicKey;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Chats
|
||||
|
||||
- (void)changeChat:(OCTChat *)chat enteredText:(NSString *)enteredText
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[manager updateObject:chat withBlock:^(OCTChat *theChat) {
|
||||
theChat.enteredText = enteredText;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)changeChat:(OCTChat *)chat lastReadDateInterval:(NSTimeInterval)lastReadDateInterval
|
||||
{
|
||||
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
|
||||
|
||||
[manager updateObject:chat withBlock:^(OCTChat *theChat) {
|
||||
theChat.lastReadDateInterval = lastReadDateInterval;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (Class)classForFetchRequestType:(OCTFetchRequestType)type
|
||||
{
|
||||
switch (type) {
|
||||
case OCTFetchRequestTypeFriend:
|
||||
return [OCTFriend class];
|
||||
case OCTFetchRequestTypeFriendRequest:
|
||||
return [OCTFriendRequest class];
|
||||
case OCTFetchRequestTypeChat:
|
||||
return [OCTChat class];
|
||||
case OCTFetchRequestTypeCall:
|
||||
return [OCTCall class];
|
||||
case OCTFetchRequestTypeMessageAbstract:
|
||||
return [OCTMessageAbstract class];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,16 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerDataSource.h"
|
||||
#import "OCTToxDelegate.h"
|
||||
|
||||
@protocol OCTSubmanagerProtocol <OCTToxDelegate>
|
||||
|
||||
@property (weak, nonatomic) id<OCTSubmanagerDataSource> dataSource;
|
||||
|
||||
@optional
|
||||
|
||||
- (void)configure;
|
||||
|
||||
@end
|
@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerUser.h"
|
||||
#import "OCTSubmanagerProtocol.h"
|
||||
|
||||
@interface OCTSubmanagerUserImpl : NSObject <OCTSubmanagerUser, OCTSubmanagerProtocol>
|
||||
|
||||
@end
|
@ -0,0 +1,132 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTSubmanagerUserImpl.h"
|
||||
#import "OCTTox.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
#import "OCTRealmManager.h"
|
||||
#import "OCTSettingsStorageObject.h"
|
||||
|
||||
@implementation OCTSubmanagerUserImpl
|
||||
@synthesize delegate = _delegate;
|
||||
@synthesize dataSource = _dataSource;
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (OCTToxConnectionStatus)connectionStatus
|
||||
{
|
||||
return [self.dataSource managerGetTox].connectionStatus;
|
||||
}
|
||||
|
||||
- (NSString *)userAddress
|
||||
{
|
||||
return [self.dataSource managerGetTox].userAddress;
|
||||
}
|
||||
|
||||
- (OCTToxCapabilities)capabilities
|
||||
{
|
||||
return [self.dataSource managerGetTox].capabilities;
|
||||
}
|
||||
|
||||
- (NSString *)publicKey
|
||||
{
|
||||
return [self.dataSource managerGetTox].publicKey;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (OCTToxNoSpam)nospam
|
||||
{
|
||||
return [self.dataSource managerGetTox].nospam;
|
||||
}
|
||||
|
||||
- (void)setNospam:(OCTToxNoSpam)nospam
|
||||
{
|
||||
[self.dataSource managerGetTox].nospam = nospam;
|
||||
[self.dataSource managerSaveTox];
|
||||
}
|
||||
|
||||
- (OCTToxUserStatus)userStatus
|
||||
{
|
||||
return [self.dataSource managerGetTox].userStatus;
|
||||
}
|
||||
|
||||
- (void)setUserStatus:(OCTToxUserStatus)userStatus
|
||||
{
|
||||
[self.dataSource managerGetTox].userStatus = userStatus;
|
||||
[self.dataSource managerSaveTox];
|
||||
}
|
||||
|
||||
- (BOOL)setUserName:(NSString *)name error:(NSError **)error
|
||||
{
|
||||
if ([[self.dataSource managerGetTox] setNickname:name error:error]) {
|
||||
[self.dataSource managerSaveTox];
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSString *)userName
|
||||
{
|
||||
return [[self.dataSource managerGetTox] userName];
|
||||
}
|
||||
|
||||
- (BOOL)setUserStatusMessage:(NSString *)statusMessage error:(NSError **)error
|
||||
{
|
||||
if ([[self.dataSource managerGetTox] setUserStatusMessage:statusMessage error:error]) {
|
||||
[self.dataSource managerSaveTox];
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSString *)userStatusMessage
|
||||
{
|
||||
return [[self.dataSource managerGetTox] userStatusMessage];
|
||||
}
|
||||
|
||||
- (BOOL)setUserAvatar:(NSData *)avatar error:(NSError **)error
|
||||
{
|
||||
if (avatar && (avatar.length > kOCTManagerMaxAvatarSize)) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:kOCTManagerErrorDomain
|
||||
code:OCTSetUserAvatarErrorTooBig
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : @"Cannot set user avatar",
|
||||
NSLocalizedFailureReasonErrorKey : @"Avatar is too big",
|
||||
}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
OCTRealmManager *realmManager = self.dataSource.managerGetRealmManager;
|
||||
|
||||
[realmManager updateObject:realmManager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
|
||||
object.userAvatarData = avatar;
|
||||
}];
|
||||
|
||||
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTUserAvatarWasUpdatedNotification object:nil];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSData *)userAvatar
|
||||
{
|
||||
return self.dataSource.managerGetRealmManager.settingsStorage.userAvatarData;
|
||||
}
|
||||
|
||||
#pragma mark - OCTToxDelegate
|
||||
|
||||
- (void)tox:(OCTTox *)tox connectionStatus:(OCTToxConnectionStatus)connectionStatus
|
||||
{
|
||||
if (connectionStatus != OCTToxConnectionStatusNone) {
|
||||
[self.dataSource managerSaveTox];
|
||||
}
|
||||
|
||||
[self.delegate submanagerUser:self connectionStatusUpdate:connectionStatus];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,23 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTToxAVConstants.h"
|
||||
@import CoreVideo;
|
||||
|
||||
/**
|
||||
* This class helps with allocating and keeping CVPixelBuffers.
|
||||
*/
|
||||
@interface OCTPixelBufferPool : NSObject
|
||||
|
||||
- (instancetype)initWithFormat:(OSType)format;
|
||||
|
||||
/**
|
||||
* Grab a pixel buffer from the pool.
|
||||
* @param bufferRef Reference to the buffer ref.
|
||||
* @return YES on success, NO otherwise.
|
||||
*/
|
||||
- (BOOL)createPixelBuffer:(CVPixelBufferRef *)bufferRef width:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height;
|
||||
|
||||
@end
|
@ -0,0 +1,103 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTPixelBufferPool.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@interface OCTPixelBufferPool ()
|
||||
|
||||
@property (nonatomic, assign) CVPixelBufferPoolRef pool;
|
||||
@property (nonatomic, assign) OSType formatType;
|
||||
@property (nonatomic, assign) OCTToxAVVideoWidth width;
|
||||
@property (nonatomic, assign) OCTToxAVVideoHeight height;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTPixelBufferPool
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFormat:(OSType)format;
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_formatType = format;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (self.pool) {
|
||||
CFRelease(self.pool);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)createPixelBuffer:(CVPixelBufferRef *)bufferRef width:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height
|
||||
{
|
||||
BOOL success = YES;
|
||||
if (! self.pool) {
|
||||
success = [self createPoolWithWidth:width height:height format:self.formatType];
|
||||
}
|
||||
|
||||
if ((self.width != width) || (self.height != height)) {
|
||||
success = [self createPoolWithWidth:width height:height format:self.formatType];
|
||||
}
|
||||
|
||||
if (! success) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [self createPixelBuffer:bufferRef];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (BOOL)createPoolWithWidth:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height format:(OSType)format
|
||||
{
|
||||
if (self.pool) {
|
||||
CFRelease(self.pool);
|
||||
}
|
||||
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
|
||||
NSDictionary *pixelBufferAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{},
|
||||
(id)kCVPixelBufferHeightKey : @(height),
|
||||
(id)kCVPixelBufferWidthKey : @(width),
|
||||
(id)kCVPixelBufferPixelFormatTypeKey : @(format)};
|
||||
|
||||
CVReturn success = CVPixelBufferPoolCreate(kCFAllocatorDefault,
|
||||
NULL,
|
||||
(__bridge CFDictionaryRef)(pixelBufferAttributes),
|
||||
&_pool);
|
||||
|
||||
if (success != kCVReturnSuccess) {
|
||||
OCTLogWarn(@"failed to create CVPixelBufferPool error:%d", success);
|
||||
|
||||
}
|
||||
|
||||
return (success == kCVReturnSuccess);
|
||||
}
|
||||
|
||||
- (BOOL)createPixelBuffer:(CVPixelBufferRef *)bufferRef
|
||||
{
|
||||
CVReturn success = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault,
|
||||
self.pool,
|
||||
bufferRef);
|
||||
|
||||
if (success != kCVReturnSuccess) {
|
||||
OCTLogWarn(@"Failed to create pixelBuffer error:%d", success);
|
||||
}
|
||||
|
||||
return (success == kCVReturnSuccess);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,118 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OCTView.h"
|
||||
|
||||
#import "OCTToxAV.h"
|
||||
|
||||
@interface OCTVideoEngine : NSObject
|
||||
|
||||
@property (weak, nonatomic) OCTToxAV *toxav;
|
||||
|
||||
/**
|
||||
* Current friend number that video engine should
|
||||
* process video data to and from.
|
||||
*/
|
||||
@property (nonatomic, assign) OCTToxFriendNumber friendNumber;
|
||||
|
||||
/**
|
||||
* This must be called prior to using the video session.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES if successful, otherwise NO.
|
||||
*/
|
||||
- (BOOL)setupAndReturnError:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Start sending video data.
|
||||
* This will turn on processIncomingVideo to YES
|
||||
*/
|
||||
- (void)startSendingVideo;
|
||||
|
||||
/**
|
||||
* Stop sending video data.
|
||||
* This will turn off processIncomingVideo to NO
|
||||
*/
|
||||
- (void)stopSendingVideo;
|
||||
|
||||
/**
|
||||
* Indicates if the video engine is sending video.
|
||||
* @return YES if running, NO otherwise.
|
||||
*/
|
||||
- (BOOL)isSendingVideo;
|
||||
|
||||
/**
|
||||
* Generate a OCTView with the current incoming video feed.
|
||||
*/
|
||||
- (OCTView *)videoFeed;
|
||||
|
||||
/**
|
||||
* Layer of the preview video.
|
||||
* Layer will be nil if videoSession is not running.
|
||||
* @param completionBlock Block responsible for using the layer. This
|
||||
* must not be nil.
|
||||
*/
|
||||
- (void)getVideoCallPreview:(void (^)(CALayer *layer))completionBlock;
|
||||
|
||||
/**
|
||||
* Provide video frames to video engine to process.
|
||||
* @param width Width of the frame in pixels.
|
||||
* @param height Height of the frame in pixels.
|
||||
* @param yPlane
|
||||
* @param uPlane
|
||||
* @param vPlane Plane data.
|
||||
* The size of plane data is derived from width and height where
|
||||
* Y = MAX(width, abs(ystride)) * height,
|
||||
* U = MAX(width/2, abs(ustride)) * (height/2) and
|
||||
* V = MAX(width/2, abs(vstride)) * (height/2).
|
||||
* @param yStride
|
||||
* @param uStride
|
||||
* @param vStride Strides data. Strides represent padding for each plane
|
||||
* that may or may not be present. You must handle strides in
|
||||
* your image processing code. Strides are negative if the
|
||||
* image is bottom-up hence why you MUST abs() it when
|
||||
* calculating plane buffer size.
|
||||
* @param friendNumber The friend number of the friend who sent an audio frame.
|
||||
*
|
||||
*/
|
||||
- (void)receiveVideoFrameWithWidth:(OCTToxAVVideoWidth)width
|
||||
height:(OCTToxAVVideoHeight)height
|
||||
yPlane:(OCTToxAVPlaneData *)yPlane
|
||||
uPlane:(OCTToxAVPlaneData *)uPlane
|
||||
vPlane:(OCTToxAVPlaneData *)vPlane
|
||||
yStride:(OCTToxAVStrideData)yStride
|
||||
uStride:(OCTToxAVStrideData)uStride
|
||||
vStride:(OCTToxAVStrideData)vStride
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber;
|
||||
@end
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
@interface OCTVideoEngine (MacDevice)
|
||||
|
||||
/**
|
||||
* Use a different camera for input.
|
||||
* @param camera The camera's AVFoundation device ID.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)switchToCamera:(NSString *)camera error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#else
|
||||
|
||||
@interface OCTVideoEngine (iOSDevice)
|
||||
|
||||
/**
|
||||
* Use a different camera for input.
|
||||
* @param camera The camera's AVFoundation device ID.
|
||||
* @param error Pointer to error object.
|
||||
* @return YES on success, otherwise NO.
|
||||
*/
|
||||
- (BOOL)useFrontCamera:(BOOL)front error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
@ -0,0 +1,489 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTVideoEngine.h"
|
||||
#import "OCTVideoView.h"
|
||||
#import "OCTPixelBufferPool.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
#import "OCTLogging.h"
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
static const OSType kPixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
|
||||
|
||||
@interface OCTVideoEngine () <AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
|
||||
@property (nonatomic, strong) AVCaptureSession *captureSession;
|
||||
@property (nonatomic, strong) AVCaptureVideoDataOutput *dataOutput;
|
||||
@property (nonatomic, strong) dispatch_queue_t processingQueue;
|
||||
@property (nonatomic, strong) OCTVideoView *videoView;
|
||||
@property (nonatomic, weak) AVCaptureVideoPreviewLayer *previewLayer;
|
||||
@property (nonatomic, assign) uint8_t *reusableUChromaPlane;
|
||||
@property (nonatomic, assign) uint8_t *reusableVChromaPlane;
|
||||
@property (nonatomic, assign) uint8_t *reusableYChromaPlane;
|
||||
@property (strong, nonatomic) OCTPixelBufferPool *pixelPool;
|
||||
@property (nonatomic, assign) NSUInteger sizeOfChromaPlanes;
|
||||
@property (nonatomic, assign) NSUInteger sizeOfYPlane;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTVideoEngine
|
||||
|
||||
#pragma mark - Life cycle
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (! self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
OCTLogVerbose(@"init");
|
||||
|
||||
// Disabling captureSession for simulator due to bug in iOS 10.
|
||||
// See https://forums.developer.apple.com/thread/62230
|
||||
#if ! TARGET_OS_SIMULATOR
|
||||
_captureSession = [AVCaptureSession new];
|
||||
_captureSession.sessionPreset = AVCaptureSessionPresetMedium;
|
||||
|
||||
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
|
||||
_captureSession.sessionPreset = AVCaptureSessionPreset640x480;
|
||||
}
|
||||
#endif
|
||||
|
||||
_dataOutput = [AVCaptureVideoDataOutput new];
|
||||
_processingQueue = dispatch_queue_create("me.dvor.objcTox.OCTVideoEngineQueue", NULL);
|
||||
_pixelPool = [[OCTPixelBufferPool alloc] initWithFormat:kPixelFormat];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (self.reusableUChromaPlane) {
|
||||
free(self.reusableUChromaPlane);
|
||||
}
|
||||
|
||||
if (self.reusableVChromaPlane) {
|
||||
free(self.reusableVChromaPlane);
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)setupAndReturnError:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"setupAndReturnError");
|
||||
#if TARGET_OS_IPHONE
|
||||
AVCaptureDevice *videoCaptureDevice = [self getDeviceForPosition:AVCaptureDevicePositionFront];
|
||||
#else
|
||||
AVCaptureDevice *videoCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
#endif
|
||||
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoCaptureDevice error:error];
|
||||
|
||||
if (! videoInput) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([self.captureSession canAddInput:videoInput]) {
|
||||
[self.captureSession addInput:videoInput];
|
||||
}
|
||||
|
||||
self.dataOutput.alwaysDiscardsLateVideoFrames = YES;
|
||||
self.dataOutput.videoSettings = @{
|
||||
(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kPixelFormat),
|
||||
};
|
||||
[self.dataOutput setSampleBufferDelegate:self queue:self.processingQueue];
|
||||
|
||||
[self.captureSession addOutput:self.dataOutput];
|
||||
AVCaptureConnection *conn = [self.dataOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
|
||||
if (conn.supportsVideoOrientation) {
|
||||
[self registerOrientationNotification];
|
||||
[self orientationChanged];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)switchToCamera:(NSString *)camera error:(NSError **)error
|
||||
{
|
||||
AVCaptureDevice *dev = nil;
|
||||
|
||||
if (! camera) {
|
||||
dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
}
|
||||
else {
|
||||
dev = [AVCaptureDevice deviceWithUniqueID:camera];
|
||||
}
|
||||
|
||||
return [self actuallySetCamera:dev error:error];
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
- (BOOL)useFrontCamera:(BOOL)front error:(NSError **)error
|
||||
{
|
||||
AVCaptureDevicePosition position = front ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
|
||||
|
||||
AVCaptureDevice *dev = [self getDeviceForPosition:position];
|
||||
|
||||
return [self actuallySetCamera:dev error:error];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)actuallySetCamera:(AVCaptureDevice *)dev error:(NSError **)error
|
||||
{
|
||||
OCTLogVerbose(@"actuallySetCamera: %@", dev);
|
||||
|
||||
NSArray *inputs = [self.captureSession inputs];
|
||||
|
||||
AVCaptureInput *current = [inputs firstObject];
|
||||
if ([current isKindOfClass:[AVCaptureDeviceInput class]]) {
|
||||
AVCaptureDeviceInput *inputDevice = (AVCaptureDeviceInput *)current;
|
||||
if ([inputDevice.device.uniqueID isEqualToString:dev.uniqueID]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
for (AVCaptureInput *input in inputs) {
|
||||
[self.captureSession removeInput:input];
|
||||
}
|
||||
|
||||
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:dev error:error];
|
||||
|
||||
if (! videoInput) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (! [self.captureSession canAddInput:videoInput]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.captureSession addInput:videoInput];
|
||||
|
||||
[self orientationChanged];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)startSendingVideo
|
||||
{
|
||||
OCTLogVerbose(@"startSendingVideo");
|
||||
|
||||
dispatch_async(self.processingQueue, ^{
|
||||
if ([self isSendingVideo]) {
|
||||
return;
|
||||
}
|
||||
[self.captureSession startRunning];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopSendingVideo
|
||||
{
|
||||
OCTLogVerbose(@"stopSendingVideo");
|
||||
|
||||
dispatch_async(self.processingQueue, ^{
|
||||
|
||||
if (! [self isSendingVideo]) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.captureSession stopRunning];
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)isSendingVideo
|
||||
{
|
||||
OCTLogVerbose(@"isSendingVideo");
|
||||
return self.captureSession.isRunning;
|
||||
}
|
||||
|
||||
- (void)getVideoCallPreview:(void (^)(CALayer *))completionBlock
|
||||
{
|
||||
NSParameterAssert(completionBlock);
|
||||
OCTLogVerbose(@"videoCallPreview");
|
||||
dispatch_async(self.processingQueue, ^{
|
||||
AVCaptureVideoPreviewLayer *previewLayer = self.previewLayer;
|
||||
|
||||
if (! self.previewLayer) {
|
||||
previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completionBlock(previewLayer);
|
||||
});
|
||||
|
||||
self.previewLayer = previewLayer;
|
||||
});
|
||||
}
|
||||
|
||||
- (OCTView *)videoFeed;
|
||||
{
|
||||
OCTLogVerbose(@"videoFeed");
|
||||
|
||||
OCTVideoView *feed = self.videoView;
|
||||
|
||||
if (! feed) {
|
||||
feed = [OCTVideoView view];
|
||||
self.videoView = feed;
|
||||
}
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
- (void)receiveVideoFrameWithWidth:(OCTToxAVVideoWidth)width
|
||||
height:(OCTToxAVVideoHeight)height
|
||||
yPlane:(OCTToxAVPlaneData *)yPlane
|
||||
uPlane:(OCTToxAVPlaneData *)uPlane
|
||||
vPlane:(OCTToxAVPlaneData *)vPlane
|
||||
yStride:(OCTToxAVStrideData)yStride
|
||||
uStride:(OCTToxAVStrideData)uStride
|
||||
vStride:(OCTToxAVStrideData)vStride
|
||||
friendNumber:(OCTToxFriendNumber)friendNumber
|
||||
{
|
||||
|
||||
if (! self.videoView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! yPlane) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! uPlane) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! vPlane) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
size_t yBytesPerRow = MIN(width, abs(yStride));
|
||||
size_t uvBytesPerRow = MIN(width / 2, abs(uStride));
|
||||
|
||||
/**
|
||||
* Create pixel buffers and copy YUV planes over
|
||||
*/
|
||||
CVPixelBufferRef bufferRef = NULL;
|
||||
|
||||
if (! [self.pixelPool createPixelBuffer:&bufferRef width:width height:height]) {
|
||||
return;
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(bufferRef, 0);
|
||||
|
||||
OCTToxAVPlaneData *ySource = yPlane;
|
||||
// if stride is negative, start reading from the left of the last row
|
||||
if (yStride < 0) {
|
||||
ySource = ySource + ((-yStride) * (height - 1));
|
||||
}
|
||||
|
||||
uint8_t *yDestinationPlane = CVPixelBufferGetBaseAddressOfPlane(bufferRef, 0);
|
||||
size_t yDestinationStride = CVPixelBufferGetBytesPerRowOfPlane(bufferRef, 0);
|
||||
|
||||
/* Copy yPlane data */
|
||||
for (size_t yHeight = 0; yHeight < height; yHeight++) {
|
||||
memcpy(yDestinationPlane, ySource, yBytesPerRow);
|
||||
ySource += yStride;
|
||||
yDestinationPlane += yDestinationStride;
|
||||
}
|
||||
|
||||
/* Interweave U and V */
|
||||
uint8_t *uvDestinationPlane = CVPixelBufferGetBaseAddressOfPlane(bufferRef, 1);
|
||||
size_t uvDestinationStride = CVPixelBufferGetBytesPerRowOfPlane(bufferRef, 1);
|
||||
|
||||
OCTToxAVPlaneData *uSource = uPlane;
|
||||
if (uStride < 0) {
|
||||
uSource = uSource + ((-uStride) * ((height / 2) - 1));
|
||||
}
|
||||
|
||||
OCTToxAVPlaneData *vSource = vPlane;
|
||||
if (vStride < 0) {
|
||||
vSource = vSource + ((-vStride) * ((height / 2) - 1));
|
||||
}
|
||||
|
||||
for (size_t yHeight = 0; yHeight < height / 2; yHeight++) {
|
||||
for (size_t index = 0; index < uvBytesPerRow; index++) {
|
||||
uvDestinationPlane[index * 2] = uSource[index];
|
||||
uvDestinationPlane[(index * 2) + 1] = vSource[index];
|
||||
}
|
||||
uvDestinationPlane += uvDestinationStride;
|
||||
uSource += uStride;
|
||||
vSource += vStride;
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(bufferRef, 0);
|
||||
|
||||
dispatch_async(self.processingQueue, ^{
|
||||
/* Create Core Image */
|
||||
CIImage *coreImage = [CIImage imageWithCVPixelBuffer:bufferRef];
|
||||
|
||||
CVPixelBufferRelease(bufferRef);
|
||||
|
||||
self.videoView.image = coreImage;
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Buffer Delegate
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
|
||||
{
|
||||
|
||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
|
||||
if (! imageBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
size_t yHeight = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
|
||||
size_t yBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
|
||||
size_t yStride = MAX(CVPixelBufferGetWidthOfPlane(imageBuffer, 0), yBytesPerRow);
|
||||
|
||||
size_t uvHeight = CVPixelBufferGetHeightOfPlane(imageBuffer, 1);
|
||||
size_t uvBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
|
||||
size_t uvStride = MAX(CVPixelBufferGetWidthOfPlane(imageBuffer, 1), uvBytesPerRow);
|
||||
|
||||
size_t ySize = yBytesPerRow * yHeight;
|
||||
size_t numberOfElementsForChroma = uvBytesPerRow * uvHeight / 2;
|
||||
|
||||
/**
|
||||
* Recreate the buffers if the original ones are too small
|
||||
*/
|
||||
if (numberOfElementsForChroma > self.sizeOfChromaPlanes) {
|
||||
|
||||
if (self.reusableUChromaPlane) {
|
||||
free(self.reusableUChromaPlane);
|
||||
}
|
||||
|
||||
if (self.reusableVChromaPlane) {
|
||||
free(self.reusableVChromaPlane);
|
||||
}
|
||||
|
||||
self.reusableUChromaPlane = malloc(numberOfElementsForChroma * sizeof(OCTToxAVPlaneData));
|
||||
self.reusableVChromaPlane = malloc(numberOfElementsForChroma * sizeof(OCTToxAVPlaneData));
|
||||
|
||||
self.sizeOfChromaPlanes = numberOfElementsForChroma;
|
||||
}
|
||||
|
||||
if (ySize > self.sizeOfYPlane) {
|
||||
if (self.reusableYChromaPlane) {
|
||||
free(self.reusableYChromaPlane);
|
||||
}
|
||||
self.reusableYChromaPlane = malloc(ySize * sizeof(OCTToxAVPlaneData));
|
||||
self.sizeOfYPlane = ySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the Y plane data while skipping stride
|
||||
*/
|
||||
OCTToxAVPlaneData *yPlane = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
||||
uint8_t *yDestination = self.reusableYChromaPlane;
|
||||
for (size_t i = 0; i < yHeight; i++) {
|
||||
memcpy(yDestination, yPlane, yBytesPerRow);
|
||||
yPlane += yStride;
|
||||
yDestination += yBytesPerRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deinterleaved the UV [uvuvuvuv] planes and place them to in the reusable arrays
|
||||
*/
|
||||
OCTToxAVPlaneData *uvPlane = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
|
||||
uint8_t *uDestination = self.reusableUChromaPlane;
|
||||
uint8_t *vDestination = self.reusableVChromaPlane;
|
||||
|
||||
for (size_t height = 0; height < uvHeight; height++) {
|
||||
|
||||
for (size_t i = 0; i < uvBytesPerRow; i += 2) {
|
||||
uDestination[i / 2] = uvPlane[i];
|
||||
vDestination[i / 2] = uvPlane[i + 1];
|
||||
}
|
||||
|
||||
uvPlane += uvStride;
|
||||
uDestination += uvBytesPerRow / 2;
|
||||
vDestination += uvBytesPerRow / 2;
|
||||
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
|
||||
uDestination = nil;
|
||||
vDestination = nil;
|
||||
|
||||
NSError *error;
|
||||
if (! [self.toxav sendVideoFrametoFriend:self.friendNumber
|
||||
width:(OCTToxAVVideoWidth)yBytesPerRow
|
||||
height:(OCTToxAVVideoHeight)yHeight
|
||||
yPlane:self.reusableYChromaPlane
|
||||
uPlane:self.reusableUChromaPlane
|
||||
vPlane:self.reusableVChromaPlane
|
||||
error:&error]) {
|
||||
OCTLogWarn(@"error:%@ width:%zu height:%zu", error, yBytesPerRow, yHeight);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (AVCaptureDevice *)getDeviceForPosition:(AVCaptureDevicePosition)position
|
||||
{
|
||||
OCTLogVerbose(@"getDeviceForPosition");
|
||||
|
||||
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
if ([device position] == position) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)registerOrientationNotification
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(orientationChanged)
|
||||
name:UIDeviceOrientationDidChangeNotification
|
||||
object:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)orientationChanged
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
|
||||
AVCaptureConnection *conn = [self.dataOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
AVCaptureVideoOrientation orientation;
|
||||
|
||||
switch (deviceOrientation) {
|
||||
case UIInterfaceOrientationPortraitUpsideDown:
|
||||
orientation = AVCaptureVideoOrientationPortraitUpsideDown;
|
||||
break;
|
||||
case UIDeviceOrientationPortrait:
|
||||
orientation = AVCaptureVideoOrientationPortrait;
|
||||
break;
|
||||
/* Landscapes are reversed, otherwise for some reason the video will be upside down */
|
||||
case UIDeviceOrientationLandscapeLeft:
|
||||
orientation = AVCaptureVideoOrientationLandscapeRight;
|
||||
break;
|
||||
case UIDeviceOrientationLandscapeRight:
|
||||
orientation = AVCaptureVideoOrientationLandscapeLeft;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
conn.videoOrientation = orientation;
|
||||
self.previewLayer.connection.videoOrientation = orientation;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,22 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTView.h"
|
||||
|
||||
@import GLKit;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@interface OCTVideoView : GLKView
|
||||
#else
|
||||
@interface OCTVideoView : NSOpenGLView
|
||||
#endif
|
||||
|
||||
@property (strong, nonatomic) CIImage *image;
|
||||
|
||||
/**
|
||||
* Allocs and calls the platform-specific initializers.
|
||||
*/
|
||||
+ (instancetype)view;
|
||||
|
||||
@end
|
@ -0,0 +1,100 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#import "OCTVideoView.h"
|
||||
#import "OCTManagerConstants.h"
|
||||
#import "OCTLogging.h"
|
||||
@import Foundation;
|
||||
@import AVFoundation;
|
||||
|
||||
@interface OCTVideoView ()
|
||||
|
||||
@property (strong, nonatomic) CIContext *coreImageContext;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OCTVideoView
|
||||
|
||||
+ (instancetype)view
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
OCTVideoView *videoView = [[self alloc] initWithFrame:CGRectZero];
|
||||
#else
|
||||
OCTVideoView *videoView = [[self alloc] initWithFrame:CGRectZero pixelFormat:[self defaultPixelFormat]];
|
||||
#endif
|
||||
[videoView finishInitializing];
|
||||
return videoView;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
OCTLogVerbose(@"dealloc");
|
||||
}
|
||||
|
||||
- (void)finishInitializing
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
__weak OCTVideoView *weakSelf = self;
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
||||
OCTVideoView *strongSelf = weakSelf;
|
||||
strongSelf.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
||||
strongSelf.coreImageContext = [CIContext contextWithEAGLContext:strongSelf.context];
|
||||
});
|
||||
|
||||
self.enableSetNeedsDisplay = NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setImage:(CIImage *)image
|
||||
{
|
||||
_image = image;
|
||||
#if TARGET_OS_IPHONE
|
||||
[self display];
|
||||
#else
|
||||
[self setNeedsDisplay:YES];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
// OS X: we need to correct the viewport when the view size changes
|
||||
- (void)reshape
|
||||
{
|
||||
glViewport(0, 0, self.bounds.size.width, self.bounds.size.height);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
if (self.image) {
|
||||
|
||||
glClearColor(0, 0.0, 0.0, 1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
CGRect destRect = AVMakeRectWithAspectRatioInsideRect(self.image.extent.size, rect);
|
||||
|
||||
float screenscale = self.window.screen.scale;
|
||||
|
||||
destRect = CGRectApplyAffineTransform(destRect, CGAffineTransformMakeScale(screenscale, screenscale));
|
||||
|
||||
[self.coreImageContext drawImage:self.image inRect:destRect fromRect:self.image.extent];
|
||||
}
|
||||
#else
|
||||
[self.openGLContext makeCurrentContext];
|
||||
|
||||
if (self.image) {
|
||||
CIContext *ctx = [CIContext contextWithCGLContext:self.openGLContext.CGLContextObj pixelFormat:self.openGLContext.pixelFormat.CGLPixelFormatObj colorSpace:nil options:nil];
|
||||
// The GL coordinate system goes from -1 to 1 on all axes by default.
|
||||
// We didn't set a matrix so use that instead of bounds.
|
||||
[ctx drawImage:self.image inRect:(CGRect) {-1, -1, 2, 2} fromRect:self.image.extent];
|
||||
}
|
||||
else {
|
||||
glClearColor(0.0, 0.0, 0.0, 1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
glFlush();
|
||||
#endif
|
||||
}
|
||||
@end
|
Reference in New Issue
Block a user