Signal-iOS/Signal/src/ViewControllers/AppSettings/OWSLinkedDevicesTableViewController.m
2019-07-25 10:25:45 -03:00

421 lines
16 KiB
Objective-C

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSLinkedDevicesTableViewController.h"
#import "OWSDeviceTableViewCell.h"
#import "OWSLinkDeviceViewController.h"
#import "Signal-Swift.h"
#import "UIViewController+Permissions.h"
#import <SignalServiceKit/NSTimer+OWS.h>
#import <SignalServiceKit/OWSDevice.h>
#import <SignalServiceKit/OWSDevicesService.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/TSDatabaseView.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSLinkedDevicesTableViewController ()
@property (nonatomic) NSArray<OWSDevice *> *devices;
@property (nonatomic) NSTimer *pollingRefreshTimer;
@property (nonatomic) BOOL isExpectingMoreDevices;
@end
#pragma mark -
int const OWSLinkedDevicesTableViewControllerSectionExistingDevices = 0;
int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1;
@implementation OWSLinkedDevicesTableViewController
- (void)dealloc
{
OWSLogVerbose(@"");
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Dependencies
- (SDSDatabaseStorage *)databaseStorage
{
return SDSDatabaseStorage.shared;
}
#pragma mark - UIViewController overrides
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = Theme.backgroundColor;
self.title = NSLocalizedString(@"LINKED_DEVICES_TITLE", @"Menu item and navbar title for the device manager");
self.isExpectingMoreDevices = NO;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 60;
self.tableView.separatorColor = Theme.cellSeparatorColor;
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"AddNewDevice"];
[self.tableView registerClass:[OWSDeviceTableViewCell class] forCellReuseIdentifier:@"ExistingDevice"];
[self.tableView applyScrollViewInsetsFix];
// GRDB TODO: Remove the yap notifications.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModified:)
name:YapDatabaseModifiedNotification
object:OWSPrimaryStorage.sharedManager.dbNotificationObject];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModifiedExternally:)
name:YapDatabaseModifiedExternallyNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceListUpdateSucceeded:)
name:NSNotificationName_DeviceListUpdateSucceeded
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceListUpdateFailed:)
name:NSNotificationName_DeviceListUpdateFailed
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceListUpdateModifiedDeviceList:)
name:NSNotificationName_DeviceListUpdateModifiedDeviceList
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceDidChange:)
name:NSNotificationName_OWSDeviceDidChange
object:nil];
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget:self action:@selector(refreshDevices) forControlEvents:UIControlEventValueChanged];
[self updateDeviceList];
}
- (void)updateDeviceList
{
OWSAssertIsOnMainThread();
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
NSArray<OWSDevice *> *devices = [OWSDevice anyFetchAllWithTransaction:transaction];
devices = [devices filter:^(OWSDevice *device) {
return !device.isPrimaryDevice;
}];
devices = [devices sortedArrayUsingComparator:^(OWSDevice *device1, OWSDevice *device2) {
return [device2.createdAt compare:device1.createdAt];
}];
self.devices = devices;
}];
// Don't show edit button for an empty table
if (self.devices.count > 0) {
self.navigationItem.rightBarButtonItem = self.editButtonItem;
} else {
self.navigationItem.rightBarButtonItem = nil;
}
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self refreshDevices];
NSIndexPath *_Nullable selectedPath = [self.tableView indexPathForSelectedRow];
if (selectedPath) {
// HACK to unselect rows when swiping back
// http://stackoverflow.com/questions/19379510/uitableviewcell-doesnt-get-deselected-when-swiping-back-quickly
[self.tableView deselectRowAtIndexPath:selectedPath animated:animated];
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.pollingRefreshTimer invalidate];
}
#pragma mark -
- (void)expectMoreDevices
{
self.isExpectingMoreDevices = YES;
// When you delete and re-add a device, you will be returned to this view in editing mode, making your newly
// added device appear with a delete icon. Probably not what you want.
self.editing = NO;
__weak typeof(self) wself = self;
[self.pollingRefreshTimer invalidate];
self.pollingRefreshTimer = [NSTimer weakScheduledTimerWithTimeInterval:(10.0)target:wself
selector:@selector(refreshDevices)
userInfo:nil
repeats:YES];
NSString *progressText = NSLocalizedString(@"WAITING_TO_COMPLETE_DEVICE_LINK_TEXT",
@"Activity indicator title, shown upon returning to the device "
@"manager, until you complete the provisioning process on desktop");
NSAttributedString *progressTitle = [[NSAttributedString alloc] initWithString:progressText];
// HACK to get refreshControl title to align properly.
self.refreshControl.attributedTitle = progressTitle;
[self.refreshControl endRefreshing];
dispatch_async(dispatch_get_main_queue(), ^{
self.refreshControl.attributedTitle = progressTitle;
[self.refreshControl beginRefreshing];
// Needed to show refresh control programatically
[self.tableView setContentOffset:CGPointMake(0, -self.refreshControl.frame.size.height) animated:NO];
});
// END HACK to get refreshControl title to align properly.
}
- (void)refreshDevices
{
[OWSDevicesService refreshDevices];
}
- (void)deviceListUpdateSucceeded:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self.refreshControl endRefreshing];
}
- (void)deviceListUpdateFailed:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
NSError *error = notification.object;
OWSAssertDebug(error);
NSString *alertTitle = NSLocalizedString(
@"DEVICE_LIST_UPDATE_FAILED_TITLE", @"Alert title that can occur when viewing device manager.");
UIAlertController *alert = [UIAlertController alertControllerWithTitle:alertTitle
message:error.localizedDescription
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *retryAction = [UIAlertAction actionWithTitle:[CommonStrings retryButton]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self refreshDevices];
}];
[alert addAction:retryAction];
UIAlertAction *dismissAction =
[UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil];
[alert addAction:dismissAction];
[self.refreshControl endRefreshing];
[self presentAlert:alert];
}
- (void)deviceListUpdateModifiedDeviceList:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
// Got our new device, we can stop refreshing.
self.isExpectingMoreDevices = NO;
[self.pollingRefreshTimer invalidate];
dispatch_async(dispatch_get_main_queue(), ^{
self.refreshControl.attributedTitle = nil;
});
}
#pragma mark - Table view data source
- (void)yapDatabaseModifiedExternally:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self updateDeviceList];
}
- (void)yapDatabaseModified:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogVerbose(@"");
[self updateDeviceList];
}
- (void)deviceDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogVerbose(@"");
[self updateDeviceList];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
switch (section) {
case OWSLinkedDevicesTableViewControllerSectionExistingDevices:
return (NSInteger)self.devices.count;
case OWSLinkedDevicesTableViewControllerSectionAddDevice:
return 1;
default:
OWSLogError(@"Unknown section: %ld", (long)section);
return 0;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];
if (indexPath.section == OWSLinkedDevicesTableViewControllerSectionAddDevice) {
[self ows_askForCameraPermissions:^(BOOL granted) {
if (!granted) {
return;
}
[self showLinkNewDeviceView];
}];
}
}
- (void)showLinkNewDeviceView
{
OWSLinkDeviceViewController *vc = [OWSLinkDeviceViewController new];
vc.linkedDevicesTableViewController = self;
[self.navigationController pushViewController:vc animated:YES];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == OWSLinkedDevicesTableViewControllerSectionAddDevice) {
UITableViewCell *cell =
[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"AddNewDevice"];
[OWSTableItem configureCell:cell];
cell.textLabel.text
= NSLocalizedString(@"LINK_NEW_DEVICE_TITLE", @"Navigation title when scanning QR code to add new device.");
cell.detailTextLabel.text
= NSLocalizedString(@"LINK_NEW_DEVICE_SUBTITLE", @"Subheading for 'Link New Device' navigation");
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(OWSLinkedDevicesTableViewController, @"add");
return cell;
} else if (indexPath.section == OWSLinkedDevicesTableViewControllerSectionExistingDevices) {
OWSDeviceTableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"ExistingDevice" forIndexPath:indexPath];
OWSDevice *device = [self deviceForRowAtIndexPath:indexPath];
[cell configureWithDevice:device];
return cell;
} else {
OWSLogError(@"Unknown section: %@", indexPath);
return [UITableViewCell new];
}
}
- (nullable OWSDevice *)deviceForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger index = (NSUInteger)indexPath.row;
OWSAssertDebug(index >= 0);
OWSAssertDebug(index < self.devices.count);
OWSDevice *_Nullable device = self.devices[index];
OWSAssertDebug(device != nil);
return device;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return indexPath.section == OWSLinkedDevicesTableViewControllerSectionExistingDevices;
}
- (nullable NSString *)tableView:(UITableView *)tableView
titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return NSLocalizedString(@"UNLINK_ACTION", "button title for unlinking a device");
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
OWSDevice *device = [self deviceForRowAtIndexPath:indexPath];
[self
touchedUnlinkControlForDevice:device
success:^{
OWSLogInfo(@"Removing unlinked device with deviceId: %ld", (long)device.deviceId);
[self.databaseStorage writeWithBlock:^(SDSAnyWriteTransaction *transaction) {
[device anyRemoveWithTransaction:transaction];
}];
[self updateDeviceList];
}];
}
}
- (void)touchedUnlinkControlForDevice:(OWSDevice *)device success:(void (^)(void))successCallback
{
NSString *confirmationTitleFormat
= NSLocalizedString(@"UNLINK_CONFIRMATION_ALERT_TITLE", @"Alert title for confirming device deletion");
NSString *confirmationTitle = [NSString stringWithFormat:confirmationTitleFormat, device.displayName];
NSString *confirmationMessage
= NSLocalizedString(@"UNLINK_CONFIRMATION_ALERT_BODY", @"Alert message to confirm unlinking a device");
UIAlertController *alert = [UIAlertController alertControllerWithTitle:confirmationTitle
message:confirmationMessage
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[OWSAlerts cancelAction]];
UIAlertAction *unlinkAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"UNLINK_ACTION", "button title for unlinking a device")
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self unlinkDevice:device success:successCallback];
});
}];
[alert addAction:unlinkAction];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentAlert:alert];
});
}
- (void)unlinkDevice:(OWSDevice *)device success:(void (^)(void))successCallback
{
[OWSDevicesService unlinkDevice:device
success:successCallback
failure:^(NSError *error) {
NSString *title = NSLocalizedString(
@"UNLINKING_FAILED_ALERT_TITLE", @"Alert title when unlinking device fails");
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:title
message:error.localizedDescription
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *retryAction =
[UIAlertAction actionWithTitle:[CommonStrings retryButton]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *aaction) {
[self unlinkDevice:device success:successCallback];
}];
[alert addAction:retryAction];
[alert addAction:[OWSAlerts cancelAction]];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentAlert:alert];
});
}];
}
@end
NS_ASSUME_NONNULL_END