ably-cocoa/Source/ARTPushActivationStateMachi...

385 lines
16 KiB
Objective-C

#import "ARTPushActivationStateMachine+Private.h"
#import "ARTPush.h"
#import "ARTPushActivationEvent.h"
#import "ARTPushActivationState.h"
#import "ARTRest+Private.h"
#import "ARTLog.h"
#import "ARTJsonEncoder.h"
#import "ARTJsonLikeEncoder.h"
#import "ARTTypes.h"
#import "ARTLocalDevice+Private.h"
#import "ARTDeviceStorage.h"
#import "ARTDevicePushDetails.h"
#import "ARTDeviceIdentityTokenDetails.h"
#import "ARTNSMutableRequest+ARTPush.h"
#import "ARTAuth+Private.h"
#if TARGET_OS_IOS
NSString *const ARTPushActivationCurrentStateKey = @"ARTPushActivationCurrentState";
NSString *const ARTPushActivationPendingEventsKey = @"ARTPushActivationPendingEvents";
@implementation ARTPushActivationStateMachine {
ARTPushActivationEvent *_lastHandledEvent;
ARTPushActivationState *_current;
dispatch_queue_t _queue;
dispatch_queue_t _userQueue;
}
- (instancetype)initWithRest:(ARTRestInternal *const)rest
delegate:(const id<ARTPushRegistererDelegate, NSObject>)delegate {
if (self = [super init]) {
_rest = rest;
_delegate = delegate;
_queue = _rest.queue;
_userQueue = _rest.userQueue;
// Unarchiving
NSData *stateData = [rest.storage objectForKey:ARTPushActivationCurrentStateKey];
_current = [ARTPushActivationState art_unarchiveFromData:stateData];
if (!_current) {
_current = [[ARTPushActivationStateNotActivated alloc] initWithMachine:self];
} else {
if ([_current isKindOfClass:[ARTPushActivationDeprecatedPersistentState class]]) {
_current = [((ARTPushActivationDeprecatedPersistentState *) _current) migrate];
}
_current.machine = self;
}
NSData *pendingEventsData = [rest.storage objectForKey:ARTPushActivationPendingEventsKey];
_pendingEvents = [ARTPushActivationEvent art_unarchiveFromData:pendingEventsData];
if (!_pendingEvents) {
_pendingEvents = [NSMutableArray array];
}
// Due to bug #966, old versions of the library might have led us to an illegal
// persisted state: we have a deviceToken, but the persisted push state is WaitingForPushDeviceDetails.
// So we need to re-emit the GotPushDeviceDetails event that led us there.
if ([_current isKindOfClass:[ARTPushActivationStateWaitingForPushDeviceDetails class]] && rest.device_nosync.apnsDeviceToken != nil) {
[rest.logger debug:@"ARTPush: re-emitting stored device details for stuck state machine"];
[self handleEvent:[ARTPushActivationEventGotPushDeviceDetails new]];
}
}
return self;
}
- (ARTPushActivationEvent *)lastEvent {
__block ARTPushActivationEvent *ret;
dispatch_sync(_queue, ^{
ret = [self lastEvent_nosync];
});
return ret;
}
- (ARTPushActivationEvent *)lastEvent_nosync {
return _lastHandledEvent;
}
- (ARTPushActivationState *)current {
__block ARTPushActivationState *ret;
dispatch_sync(_queue, ^{
ret = [self current_nosync];
});
return ret;
}
- (ARTPushActivationState *)current_nosync {
return _current;
}
- (void)sendEvent:(ARTPushActivationEvent *)event {
dispatch_async(_queue, ^{
[self handleEvent:event];
});
}
- (void)handleEvent:(nonnull ARTPushActivationEvent *)event {
[_rest.logger debug:@"%@: handling event %@ from %@", NSStringFromClass(self.class), NSStringFromClass(event.class), NSStringFromClass(_current.class)];
_lastHandledEvent = event;
if (self.onEvent) self.onEvent(event, _current);
ARTPushActivationState *maybeNext = [_current transition:event];
if (maybeNext == nil) {
[_rest.logger debug:@"%@: enqueuing event: %@", NSStringFromClass(self.class), NSStringFromClass(event.class)];
[_pendingEvents addObject:event];
return;
}
[_rest.logger debug:@"%@: transition: %@ -> %@", NSStringFromClass(self.class), NSStringFromClass(_current.class), NSStringFromClass(maybeNext.class)];
if (self.transitions) self.transitions(event, _current, maybeNext);
_current = maybeNext;
while (true) {
ARTPushActivationEvent *pending = [_pendingEvents art_peek];
if (pending == nil) {
break;
}
[_rest.logger debug:@"%@: attempting to consume pending event: %@", NSStringFromClass(self.class), NSStringFromClass(pending.class)];
maybeNext = [_current transition:pending];
if (maybeNext == nil) {
break;
}
[_pendingEvents art_dequeue];
[_rest.logger debug:@"%@: transition: %@ -> %@", NSStringFromClass(self.class), NSStringFromClass(_current.class), NSStringFromClass(maybeNext.class)];
if (self.transitions) self.transitions(event, _current, maybeNext);
_current = maybeNext;
}
[self persist];
}
- (void)persist {
// Archiving
if ([_current isKindOfClass:[ARTPushActivationPersistentState class]]) {
[self.rest.storage setObject:[_current art_archive] forKey:ARTPushActivationCurrentStateKey];
}
[self.rest.storage setObject:[_pendingEvents art_archive] forKey:ARTPushActivationPendingEventsKey];
}
- (void)deviceRegistration:(ARTErrorInfo *)error {
#if TARGET_OS_IOS
ARTLocalDevice *local = _rest.device_nosync;
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
// Custom register
if ([delegate respondsToSelector:@selector(ablyPushCustomRegister:deviceDetails:callback:)]) {
dispatch_async(_userQueue, ^{
[delegate ablyPushCustomRegister:error deviceDetails:local callback:^(ARTDeviceIdentityTokenDetails *identityTokenDetails, ARTErrorInfo *error) {
if (error) {
// Failed
[self sendEvent:[ARTPushActivationEventGettingDeviceRegistrationFailed newWithError:error]];
}
else if (identityTokenDetails) {
// Success
[self sendEvent:[ARTPushActivationEventGotDeviceRegistration newWithIdentityTokenDetails:identityTokenDetails]];
}
else {
ARTErrorInfo *missingIdentityTokenError = [ARTErrorInfo createWithCode:0 message:@"Device Identity Token Details is expected"];
[self sendEvent:[ARTPushActivationEventGettingDeviceRegistrationFailed newWithError:missingIdentityTokenError]];
}
}];
});
return;
}
void (^doDeviceRegistration)(void) = ^{
// Asynchronous HTTP request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/deviceRegistrations"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[self->_rest defaultEncoder] encodeDeviceDetails:local error:nil];
[request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"];
[[self->_rest logger] debug:__FILE__ line:__LINE__ message:@"%@: device registration with request %@", NSStringFromClass(self.class), request];
[self->_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
if (error) {
[[self->_rest logger] error:@"%@: device registration failed (%@)", NSStringFromClass(self.class), error.localizedDescription];
[self sendEvent:[ARTPushActivationEventGettingDeviceRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]];
return;
}
NSError *decodeError = nil;
ARTDeviceIdentityTokenDetails *identityTokenDetails = [[self->_rest defaultEncoder] decodeDeviceIdentityTokenDetails:data error:&decodeError];
if (decodeError != nil) {
[[self->_rest logger] error:@"%@: decode identity token details failed (%@)", NSStringFromClass(self.class), error.localizedDescription];
[self sendEvent:[ARTPushActivationEventGettingDeviceRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:decodeError]]];
return;
}
[self sendEvent:[ARTPushActivationEventGotDeviceRegistration newWithIdentityTokenDetails:identityTokenDetails]];
}];
};
if (_rest.auth.method == ARTAuthMethodToken) {
[_rest.auth authorize:^(ARTTokenDetails *tokenDetails, NSError *error) {
doDeviceRegistration();
}];
}
else {
doDeviceRegistration();
}
#endif
}
- (void)deviceUpdateRegistration:(ARTErrorInfo *)error {
#if TARGET_OS_IOS
ARTLocalDevice *local = _rest.device_nosync;
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
// Custom register
if ([delegate respondsToSelector:@selector(ablyPushCustomRegister:deviceDetails:callback:)]) {
dispatch_async(_userQueue, ^{
[delegate ablyPushCustomRegister:error deviceDetails:local callback:^(ARTDeviceIdentityTokenDetails *identityTokenDetails, ARTErrorInfo *error) {
if (error) {
// Failed
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:error]];
}
else if (identityTokenDetails) {
// Success
[self sendEvent:[ARTPushActivationEventRegistrationSynced newWithIdentityTokenDetails:identityTokenDetails]];
}
else {
ARTErrorInfo *missingIdentityTokenError = [ARTErrorInfo createWithCode:0 message:@"Device Identity Token Details is expected"];
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:missingIdentityTokenError]];
}
}];
});
return;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:local.id]];
request.HTTPMethod = @"PATCH";
request.HTTPBody = [[_rest defaultEncoder] encode:@{
@"push": @{
@"recipient": local.push.recipient
}
} error:nil];
[request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"];
[request setDeviceAuthentication:local];
[[_rest logger] debug:__FILE__ line:__LINE__ message:@"%@: update device with request %@", NSStringFromClass(self.class), request];
[_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
if (error) {
[[self->_rest logger] error:@"%@: update device failed (%@)", NSStringFromClass(self.class), error.localizedDescription];
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]];
return;
}
[self sendEvent:[ARTPushActivationEventRegistrationSynced new]];
}];
#endif
}
- (void)syncDevice {
#if TARGET_OS_IOS
ARTLocalDevice *const local = _rest.device_nosync;
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
// Custom register
if ([delegate respondsToSelector:@selector(ablyPushCustomRegister:deviceDetails:callback:)]) {
dispatch_async(_userQueue, ^{
[delegate ablyPushCustomRegister:nil deviceDetails:local callback:^(ARTDeviceIdentityTokenDetails *identityTokenDetails, ARTErrorInfo *error) {
if (error) {
// Failed
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:error]];
}
else if (identityTokenDetails) {
// Success
[self sendEvent:[ARTPushActivationEventRegistrationSynced newWithIdentityTokenDetails:identityTokenDetails]];
}
else {
ARTErrorInfo *const missingIdentityTokenError = [ARTErrorInfo createWithCode:0 message:@"Device Identity Token Details is expected"];
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:missingIdentityTokenError]];
}
}];
});
return;
}
void (^doDeviceSync)(void) = ^{
// Asynchronous HTTP request
NSString *const path = [@"/push/deviceRegistrations" stringByAppendingPathComponent:local.id];
NSMutableURLRequest *const request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]];
request.HTTPMethod = @"PUT";
request.HTTPBody = [[self->_rest defaultEncoder] encodeDeviceDetails:local error:nil];
[request setValue:[[self->_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"];
[[self->_rest logger] debug:__FILE__ line:__LINE__ message:@"%@: sync device with request %@", NSStringFromClass(self.class), request];
[self->_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
if (error) {
[[self->_rest logger] error:@"%@: device registration failed (%@)", NSStringFromClass(self.class), error.localizedDescription];
[self sendEvent:[ARTPushActivationEventSyncRegistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]];
return;
}
[self sendEvent:[ARTPushActivationEventRegistrationSynced newWithIdentityTokenDetails:local.identityTokenDetails]];
}];
};
if (_rest.auth.method == ARTAuthMethodToken) {
[_rest.auth authorize:^(ARTTokenDetails *tokenDetails, NSError *error) {
doDeviceSync();
}];
}
else {
doDeviceSync();
}
#endif
}
- (void)deviceUnregistration:(ARTErrorInfo *)error {
#if TARGET_OS_IOS
ARTLocalDevice *local = _rest.device_nosync;
__block id delegate = self.delegate;
// Custom register
SEL customDeregisterMethodSelector = @selector(ablyPushCustomDeregister:deviceId:callback:);
if ([delegate respondsToSelector:customDeregisterMethodSelector]) {
dispatch_async(_userQueue, ^{
[delegate ablyPushCustomDeregister:error deviceId:local.id callback:^(ARTErrorInfo *error) {
if (error) {
// Failed
[self sendEvent:[ARTPushActivationEventDeregistrationFailed newWithError:error]];
}
else {
// Success
[self sendEvent:[ARTPushActivationEventDeregistered new]];
}
}];
});
return;
}
// Asynchronous HTTP request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:local.id]];
request.HTTPMethod = @"DELETE";
[request setDeviceAuthentication:local];
[[_rest logger] debug:__FILE__ line:__LINE__ message:@"%@: device deregistration with request %@", NSStringFromClass(self.class), request];
[_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
if (error) {
[[self->_rest logger] error:@"%@: device deregistration failed (%@)", NSStringFromClass(self.class), error.localizedDescription];
[self sendEvent:[ARTPushActivationEventDeregistrationFailed newWithError:[ARTErrorInfo createFromNSError:error]]];
return;
}
[[self->_rest logger] debug:__FILE__ line:__LINE__ message:@"successfully deactivate device"];
[self sendEvent:[ARTPushActivationEventDeregistered new]];
}];
#endif
}
- (void)callActivatedCallback:(ARTErrorInfo *)error {
#if TARGET_OS_IOS
dispatch_async(_userQueue, ^{
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(didActivateAblyPush:)]) {
[delegate didActivateAblyPush:error];
}
});
#endif
}
- (void)callDeactivatedCallback:(ARTErrorInfo *)error {
#if TARGET_OS_IOS
dispatch_async(_userQueue, ^{
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(didDeactivateAblyPush:)]) {
[delegate didDeactivateAblyPush:error];
}
});
#endif
}
- (void)callUpdateFailedCallback:(nullable ARTErrorInfo *)error {
#if TARGET_OS_IOS
dispatch_async(_userQueue, ^{
const id<ARTPushRegistererDelegate, NSObject> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(didAblyPushRegistrationFail:)]) {
[delegate didAblyPushRegistrationFail:error];
}
});
#endif
}
@end
#endif