mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
11 KiB
Objective-C
354 lines
11 KiB
Objective-C
//
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#import "OWSDevice.h"
|
|
#import "OWSError.h"
|
|
#import "OWSPrimaryStorage.h"
|
|
#import "ProfileManagerProtocol.h"
|
|
#import "SSKEnvironment.h"
|
|
#import "TSAccountManager.h"
|
|
#import "YapDatabaseConnection+OWS.h"
|
|
#import "YapDatabaseConnection.h"
|
|
#import "YapDatabaseTransaction.h"
|
|
#import <Mantle/MTLValueTransformer.h>
|
|
#import <SessionProtocolKit/NSDate+OWS.h>
|
|
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
|
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
uint32_t const OWSDevicePrimaryDeviceId = 1;
|
|
NSString *const kOWSPrimaryStorage_OWSDeviceCollection = @"kTSStorageManager_OWSDeviceCollection";
|
|
NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_MayHaveLinkedDevices";
|
|
|
|
@interface OWSDeviceManager ()
|
|
|
|
@property (atomic) NSDate *lastReceivedSyncMessage;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation OWSDeviceManager
|
|
|
|
+ (instancetype)sharedManager
|
|
{
|
|
static OWSDeviceManager *instance = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
instance = [[self alloc] initDefault];
|
|
});
|
|
return instance;
|
|
}
|
|
|
|
- (instancetype)initDefault
|
|
{
|
|
return [super init];
|
|
}
|
|
|
|
- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection
|
|
{
|
|
OWSAssertDebug(dbConnection);
|
|
|
|
return [dbConnection boolForKey:kOWSPrimaryStorage_MayHaveLinkedDevices
|
|
inCollection:kOWSPrimaryStorage_OWSDeviceCollection
|
|
defaultValue:YES];
|
|
}
|
|
|
|
// In order to avoid skipping necessary sync messages, the default value
|
|
// for mayHaveLinkedDevices is YES. Once we've successfully sent a
|
|
// sync message with no device messages (e.g. the service has confirmed
|
|
// that we have no linked devices), we can set mayHaveLinkedDevices to NO
|
|
// to avoid unnecessary message sends for sync messages until we learn
|
|
// of a linked device (e.g. through the device linking UI or by receiving
|
|
// a sync message, etc.).
|
|
- (void)clearMayHaveLinkedDevices
|
|
{
|
|
// Note that we write async to avoid opening transactions within transactions.
|
|
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
[transaction setObject:@(NO)
|
|
forKey:kOWSPrimaryStorage_MayHaveLinkedDevices
|
|
inCollection:kOWSPrimaryStorage_OWSDeviceCollection];
|
|
}];
|
|
}
|
|
|
|
- (void)setMayHaveLinkedDevices
|
|
{
|
|
// Note that we write async to avoid opening transactions within transactions.
|
|
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
[transaction setObject:@(YES)
|
|
forKey:kOWSPrimaryStorage_MayHaveLinkedDevices
|
|
inCollection:kOWSPrimaryStorage_OWSDeviceCollection];
|
|
}];
|
|
}
|
|
|
|
- (BOOL)hasReceivedSyncMessageInLastSeconds:(NSTimeInterval)intervalSeconds
|
|
{
|
|
return (self.lastReceivedSyncMessage && fabs(self.lastReceivedSyncMessage.timeIntervalSinceNow) < intervalSeconds);
|
|
}
|
|
|
|
- (void)setHasReceivedSyncMessage
|
|
{
|
|
self.lastReceivedSyncMessage = [NSDate new];
|
|
|
|
[self setMayHaveLinkedDevices];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@interface OWSDevice ()
|
|
|
|
@property (nonatomic) NSInteger deviceId;
|
|
@property (nonatomic, nullable) NSString *name;
|
|
@property (nonatomic) NSDate *createdAt;
|
|
@property (nonatomic) NSDate *lastSeenAt;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation OWSDevice
|
|
|
|
#pragma mark - Dependencies
|
|
|
|
+ (id<ProfileManagerProtocol>)profileManager
|
|
{
|
|
return SSKEnvironment.shared.profileManager;
|
|
}
|
|
|
|
+ (id<OWSUDManager>)udManager
|
|
{
|
|
return SSKEnvironment.shared.udManager;
|
|
}
|
|
|
|
+ (TSAccountManager *)tsAccountManager
|
|
{
|
|
return TSAccountManager.sharedInstance;
|
|
}
|
|
|
|
- (OWSIdentityManager *)identityManager
|
|
{
|
|
OWSAssertDebug(SSKEnvironment.shared.identityManager);
|
|
|
|
return SSKEnvironment.shared.identityManager;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[super saveWithTransaction:transaction];
|
|
}
|
|
|
|
+ (nullable instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error
|
|
{
|
|
OWSDevice *device = [MTLJSONAdapter modelOfClass:[self class] fromJSONDictionary:deviceAttributes error:error];
|
|
if (device.deviceId < OWSDevicePrimaryDeviceId) {
|
|
OWSFailDebug(@"Invalid device id: %lu", (unsigned long)device.deviceId);
|
|
*error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Invalid device id.");
|
|
return nil;
|
|
}
|
|
return device;
|
|
}
|
|
|
|
+ (NSDictionary *)JSONKeyPathsByPropertyKey
|
|
{
|
|
return @{
|
|
@"createdAt": @"created",
|
|
@"lastSeenAt": @"lastSeen",
|
|
@"deviceId": @"id",
|
|
@"name": @"name"
|
|
};
|
|
}
|
|
|
|
+ (MTLValueTransformer *)createdAtJSONTransformer
|
|
{
|
|
return self.millisecondTimestampToDateTransformer;
|
|
}
|
|
|
|
+ (MTLValueTransformer *)lastSeenAtJSONTransformer
|
|
{
|
|
return self.millisecondTimestampToDateTransformer;
|
|
}
|
|
|
|
+ (NSArray<OWSDevice *> *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
|
|
NSMutableArray<OWSDevice *> *result = [NSMutableArray new];
|
|
[transaction enumerateKeysAndObjectsInCollection:OWSDevice.collection
|
|
usingBlock:^(NSString *key, OWSDevice *object, BOOL *stop) {
|
|
if (![object isKindOfClass:[OWSDevice class]]) {
|
|
OWSFailDebug(@"Unexpected object in collection: %@", object.class);
|
|
return;
|
|
}
|
|
[result addObject:object];
|
|
}];
|
|
return result;
|
|
}
|
|
|
|
+ (BOOL)replaceAll:(NSArray<OWSDevice *> *)currentDevices
|
|
{
|
|
BOOL didAddOrRemove = NO;
|
|
NSMutableArray<OWSDevice *> *existingDevices = [[self allObjectsInCollection] mutableCopy];
|
|
for (OWSDevice *currentDevice in currentDevices) {
|
|
NSUInteger existingDeviceIndex = [existingDevices indexOfObject:currentDevice];
|
|
if (existingDeviceIndex == NSNotFound) {
|
|
// New Device
|
|
OWSLogInfo(@"Adding device: %@", currentDevice);
|
|
[currentDevice save];
|
|
didAddOrRemove = YES;
|
|
} else {
|
|
OWSDevice *existingDevice = existingDevices[existingDeviceIndex];
|
|
if ([existingDevice updateAttributesWithDevice:currentDevice]) {
|
|
[existingDevice save];
|
|
}
|
|
[existingDevices removeObjectAtIndex:existingDeviceIndex];
|
|
}
|
|
}
|
|
|
|
// Since we removed existing devices as we went, only stale devices remain
|
|
for (OWSDevice *staleDevice in existingDevices) {
|
|
OWSLogVerbose(@"Removing device: %@", staleDevice);
|
|
[staleDevice remove];
|
|
didAddOrRemove = YES;
|
|
}
|
|
|
|
if (didAddOrRemove) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Device changes can affect the UD access mode for a recipient,
|
|
// so we need to fetch the profile for this user to update UD access mode.
|
|
[self.profileManager fetchLocalUsersProfile];
|
|
});
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
+ (MTLValueTransformer *)millisecondTimestampToDateTransformer
|
|
{
|
|
static MTLValueTransformer *instance = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
instance = [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) {
|
|
if ([value isKindOfClass:[NSNumber class]]) {
|
|
NSNumber *number = (NSNumber *)value;
|
|
NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]];
|
|
if (result) {
|
|
*success = YES;
|
|
return result;
|
|
}
|
|
}
|
|
*success = NO;
|
|
OWSLogError(@"unable to decode date from %@", value);
|
|
*error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Unable to decode date from JSON.");
|
|
return nil;
|
|
}
|
|
reverseBlock:^id(id value, BOOL *success, NSError **error) {
|
|
if ([value isKindOfClass:[NSDate class]]) {
|
|
NSDate *date = (NSDate *)value;
|
|
NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]];
|
|
if (result) {
|
|
*success = YES;
|
|
return result;
|
|
}
|
|
}
|
|
OWSLogError(@"unable to encode date from %@", value);
|
|
*error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, @"Unable to encode date to JSON.");
|
|
*success = NO;
|
|
return nil;
|
|
}];
|
|
});
|
|
return instance;
|
|
}
|
|
|
|
+ (uint32_t)currentDeviceId
|
|
{
|
|
// Someday it may be possible to have a non-primary iOS device, but for now
|
|
// any iOS device must be the primary device.
|
|
return OWSDevicePrimaryDeviceId;
|
|
}
|
|
|
|
- (BOOL)isPrimaryDevice
|
|
{
|
|
return self.deviceId == OWSDevicePrimaryDeviceId;
|
|
}
|
|
|
|
- (NSString *)displayName
|
|
{
|
|
if (self.name) {
|
|
ECKeyPair *_Nullable identityKeyPair = self.identityManager.identityKeyPair;
|
|
OWSAssertDebug(identityKeyPair);
|
|
if (identityKeyPair) {
|
|
NSError *error;
|
|
NSString *_Nullable decryptedName =
|
|
[DeviceNames decryptDeviceNameWithBase64String:self.name identityKeyPair:identityKeyPair error:&error];
|
|
if (error) {
|
|
// Not necessarily an error; might be a legacy device name.
|
|
OWSLogError(@"Could not decrypt device name: %@", error);
|
|
} else if (decryptedName) {
|
|
return decryptedName;
|
|
}
|
|
}
|
|
|
|
return self.name;
|
|
}
|
|
|
|
if (self.deviceId == OWSDevicePrimaryDeviceId) {
|
|
return @"This Device";
|
|
}
|
|
return NSLocalizedString(@"UNNAMED_DEVICE", @"Label text in device manager for a device with no name");
|
|
}
|
|
|
|
- (BOOL)updateAttributesWithDevice:(OWSDevice *)other
|
|
{
|
|
BOOL changed = NO;
|
|
if (![self.lastSeenAt isEqual:other.lastSeenAt]) {
|
|
self.lastSeenAt = other.lastSeenAt;
|
|
changed = YES;
|
|
}
|
|
|
|
if (![self.createdAt isEqual:other.createdAt]) {
|
|
self.createdAt = other.createdAt;
|
|
changed = YES;
|
|
}
|
|
|
|
if (![self.name isEqual:other.name]) {
|
|
self.name = other.name;
|
|
changed = YES;
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
+ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
return [self numberOfKeysInCollectionWithTransaction:transaction] > 1;
|
|
}
|
|
|
|
- (BOOL)isEqual:(id)object
|
|
{
|
|
if (self == object) {
|
|
return YES;
|
|
}
|
|
|
|
if (![object isKindOfClass:[OWSDevice class]]) {
|
|
return NO;
|
|
}
|
|
|
|
return [self isEqualToDevice:(OWSDevice *)object];
|
|
}
|
|
|
|
- (BOOL)isEqualToDevice:(OWSDevice *)device
|
|
{
|
|
return self.deviceId == device.deviceId;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|