mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-30 00:02:47 +00:00
400 lines
14 KiB
Objective-C
400 lines
14 KiB
Objective-C
/*
|
|
Copyright 2016 OpenMarket Ltd
|
|
Copyright 2017 Vector Creations Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
#import "CallViewController.h"
|
|
|
|
#import "AppDelegate.h"
|
|
|
|
#import "AvatarGenerator.h"
|
|
|
|
#import "MXRoom+Riot.h"
|
|
|
|
#import "UsersDevicesViewController.h"
|
|
|
|
@interface CallViewController ()
|
|
{
|
|
// Display a gradient view above the screen
|
|
CAGradientLayer* gradientMaskLayer;
|
|
|
|
/**
|
|
Current alert (if any).
|
|
*/
|
|
MXKAlert *currentAlert;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CallViewController
|
|
|
|
#pragma mark - Class methods
|
|
|
|
+ (UINib *)nib
|
|
{
|
|
return [UINib nibWithNibName:NSStringFromClass(self.class)
|
|
bundle:[NSBundle bundleForClass:self.class]];
|
|
}
|
|
|
|
+ (instancetype)callViewController:(MXCall*)call
|
|
{
|
|
CallViewController* instance = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class)
|
|
bundle:[NSBundle bundleForClass:self.class]];
|
|
|
|
// Load the view controller's view now (buttons and views will then be available).
|
|
if ([instance respondsToSelector:@selector(loadViewIfNeeded)])
|
|
{
|
|
// iOS 9 and later
|
|
[instance loadViewIfNeeded];
|
|
}
|
|
else if (instance.view)
|
|
{
|
|
// Patch: on iOS < 9.0, we load the view by calling its getter.
|
|
}
|
|
|
|
instance.mxCall = call;
|
|
|
|
return instance;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)finalizeInit
|
|
{
|
|
[super finalizeInit];
|
|
|
|
// Setup `MXKViewControllerHandling` properties
|
|
self.defaultBarTintColor = kRiotNavBarTintColor;
|
|
self.enableBarTintColorStatusChange = NO;
|
|
self.rageShakeManager = [RageShakeManager sharedManager];
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
[self.backToAppButton setImage:[UIImage imageNamed:@"back_icon"] forState:UIControlStateNormal];
|
|
[self.backToAppButton setImage:[UIImage imageNamed:@"back_icon"] forState:UIControlStateHighlighted];
|
|
|
|
[self.cameraSwitchButton setImage:[UIImage imageNamed:@"camera_switch"] forState:UIControlStateNormal];
|
|
[self.cameraSwitchButton setImage:[UIImage imageNamed:@"camera_switch"] forState:UIControlStateHighlighted];
|
|
|
|
[self.audioMuteButton setImage:[UIImage imageNamed:@"call_audio_mute_off_icon"] forState:UIControlStateNormal];
|
|
[self.audioMuteButton setImage:[UIImage imageNamed:@"call_audio_mute_off_icon"] forState:UIControlStateHighlighted];
|
|
[self.audioMuteButton setImage:[UIImage imageNamed:@"call_audio_mute_on_icon"] forState:UIControlStateSelected];
|
|
[self.videoMuteButton setImage:[UIImage imageNamed:@"call_video_mute_off_icon"] forState:UIControlStateNormal];
|
|
[self.videoMuteButton setImage:[UIImage imageNamed:@"call_video_mute_off_icon"] forState:UIControlStateHighlighted];
|
|
[self.videoMuteButton setImage:[UIImage imageNamed:@"call_video_mute_on_icon"] forState:UIControlStateSelected];
|
|
[self.speakerButton setImage:[UIImage imageNamed:@"call_speaker_off_icon"] forState:UIControlStateNormal];
|
|
[self.speakerButton setImage:[UIImage imageNamed:@"call_speaker_on_icon"] forState:UIControlStateSelected];
|
|
[self.chatButton setImage:[UIImage imageNamed:@"call_chat_icon"] forState:UIControlStateNormal];
|
|
[self.chatButton setImage:[UIImage imageNamed:@"call_chat_icon"] forState:UIControlStateHighlighted];
|
|
|
|
[self.endCallButton setTitle:nil forState:UIControlStateNormal];
|
|
[self.endCallButton setTitle:nil forState:UIControlStateHighlighted];
|
|
[self.endCallButton setImage:[UIImage imageNamed:@"call_hangup_icon"] forState:UIControlStateNormal];
|
|
[self.endCallButton setImage:[UIImage imageNamed:@"call_hangup_icon"] forState:UIControlStateHighlighted];
|
|
|
|
self.callerNameLabel.textColor = kRiotTextColorBlack;
|
|
self.callStatusLabel.textColor = kRiotTextColorDarkGray;
|
|
|
|
self.localPreviewContainerView.layer.borderColor = kRiotColorGreen.CGColor;
|
|
self.localPreviewContainerView.layer.borderWidth = 2;
|
|
self.localPreviewContainerView.layer.cornerRadius = 5;
|
|
self.localPreviewContainerView.clipsToBounds = YES;
|
|
|
|
self.remotePreviewContainerView.backgroundColor = [UIColor whiteColor];
|
|
|
|
// Add a gradient mask programatically at the top of the screen (background of the call information (name, status))
|
|
gradientMaskLayer = [CAGradientLayer layer];
|
|
|
|
CGColorRef opaqueWhiteColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor;
|
|
CGColorRef transparentWhiteColor = [UIColor colorWithWhite:1.0 alpha:0].CGColor;
|
|
|
|
gradientMaskLayer.colors = [NSArray arrayWithObjects:(__bridge id)opaqueWhiteColor, (__bridge id)transparentWhiteColor, nil];
|
|
|
|
gradientMaskLayer.bounds = CGRectMake(0, 0, self.callContainerView.frame.size.width, self.callContainerView.frame.size.height + 20);
|
|
gradientMaskLayer.anchorPoint = CGPointZero;
|
|
|
|
// Define caller image view size
|
|
CGSize size = [[UIScreen mainScreen] bounds].size;
|
|
CGFloat minSize = MIN(size.width, size.height);
|
|
self.callerImageViewWidthConstraint.constant = minSize / 2;
|
|
|
|
// CAConstraint is not supported on IOS.
|
|
// it seems only being supported on Mac OS.
|
|
// so viewDidLayoutSubviews will refresh the layout bounds.
|
|
[self.gradientMaskContainerView.layer addSublayer:gradientMaskLayer];
|
|
|
|
[self updateLocalPreviewLayout];
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews
|
|
{
|
|
[super viewDidLayoutSubviews];
|
|
|
|
// sanity check
|
|
if (gradientMaskLayer)
|
|
{
|
|
CGRect currentBounds = gradientMaskLayer.bounds;
|
|
CGRect newBounds = CGRectMake(0, 0, self.callContainerView.frame.size.width, self.callContainerView.frame.size.height + 20);
|
|
|
|
// check if there is an update
|
|
if (!CGSizeEqualToSize(currentBounds.size, newBounds.size))
|
|
{
|
|
newBounds.origin = CGPointZero;
|
|
gradientMaskLayer.bounds = newBounds;
|
|
}
|
|
}
|
|
|
|
// The caller image view is circular
|
|
self.callerImageView.layer.cornerRadius = self.callerImageViewWidthConstraint.constant / 2;
|
|
self.callerImageView.clipsToBounds = YES;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismiss:NO];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
[super viewWillDisappear:animated];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
}
|
|
|
|
#pragma mark - override MXKViewController
|
|
|
|
- (void)destroy
|
|
{
|
|
[super destroy];
|
|
|
|
[gradientMaskLayer removeFromSuperlayer];
|
|
gradientMaskLayer = nil;
|
|
}
|
|
|
|
#pragma mark - MXCallDelegate
|
|
|
|
- (void)call:(MXCall *)call didEncounterError:(NSError *)error
|
|
{
|
|
if ([error.domain isEqualToString:MXEncryptingErrorDomain]
|
|
&& error.code == MXEncryptingErrorUnknownDeviceCode)
|
|
{
|
|
// There are unknown devices, check what the user wants to do
|
|
__weak __typeof(self) weakSelf = self;
|
|
|
|
MXUsersDevicesMap<MXDeviceInfo*> *unknownDevices = error.userInfo[MXEncryptingErrorUnknownDeviceDevicesKey];
|
|
|
|
[currentAlert dismiss:NO];
|
|
currentAlert = [[MXKAlert alloc] initWithTitle:[NSBundle mxk_localizedStringForKey:@"unknown_devices_alert_title"]
|
|
message:[NSBundle mxk_localizedStringForKey:@"unknown_devices_alert"]
|
|
style:MXKAlertStyleAlert];
|
|
|
|
[currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"unknown_devices_verify"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) {
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
// Get the UsersDevicesViewController from the storyboard
|
|
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
|
|
UsersDevicesViewController *usersDevicesViewController = [storyboard instantiateViewControllerWithIdentifier:@"UsersDevicesViewControllerStoryboardId"];
|
|
|
|
[usersDevicesViewController displayUsersDevices:unknownDevices andMatrixSession:self.mainSession onComplete:^(BOOL doneButtonPressed) {
|
|
|
|
if (doneButtonPressed)
|
|
{
|
|
// Retry the call
|
|
if (call.isIncoming)
|
|
{
|
|
[call answer];
|
|
}
|
|
else
|
|
{
|
|
[call callWithVideo:call.isVideoCall];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ignore the call
|
|
[call hangup];
|
|
}
|
|
}];
|
|
|
|
// Show this screen within a navigation controller
|
|
UINavigationController *usersDevicesNavigationController = [[UINavigationController alloc] init];
|
|
[usersDevicesNavigationController pushViewController:usersDevicesViewController animated:NO];
|
|
|
|
[self presentViewController:usersDevicesNavigationController animated:YES completion:nil];
|
|
|
|
}
|
|
}];
|
|
|
|
[currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:(call.isIncoming ? @"unknown_devices_answer_anyway":@"unknown_devices_call_anyway")]
|
|
style:MXKAlertActionStyleDefault
|
|
handler:^(MXKAlert *alert) {
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
// Acknowledge the existence of all devices
|
|
[self startActivityIndicator];
|
|
[self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{
|
|
|
|
[self stopActivityIndicator];
|
|
|
|
// Retry the call
|
|
if (call.isIncoming)
|
|
{
|
|
[call answer];
|
|
}
|
|
else
|
|
{
|
|
[call callWithVideo:call.isVideoCall];
|
|
}
|
|
}];
|
|
}
|
|
}];
|
|
|
|
currentAlert.mxkAccessibilityIdentifier = @"CallVCUnknownDevicesAlert";
|
|
[currentAlert showInViewController:self];
|
|
}
|
|
else
|
|
{
|
|
[super call:call didEncounterError:error];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
- (UIImage*)picturePlaceholder
|
|
{
|
|
CGFloat fontSize = floor(self.callerImageViewWidthConstraint.constant * 0.7);
|
|
|
|
if (self.peer)
|
|
{
|
|
// Use the vector style placeholder
|
|
return [AvatarGenerator generateAvatarForMatrixItem:self.peer.userId withDisplayName:self.peer.displayname size:self.callerImageViewWidthConstraint.constant andFontSize:fontSize];
|
|
}
|
|
else if (self.mxCall.room)
|
|
{
|
|
return [AvatarGenerator generateAvatarForMatrixItem:self.mxCall.room.roomId withDisplayName:self.mxCall.room.riotDisplayname size:self.callerImageViewWidthConstraint.constant andFontSize:fontSize];
|
|
}
|
|
|
|
return [UIImage imageNamed:@"placeholder"];
|
|
}
|
|
|
|
- (void)setMxCall:(MXCall *)call
|
|
{
|
|
[super setMxCall:call];
|
|
|
|
self.callerImageView.hidden = self.mxCall.isVideoCall;
|
|
}
|
|
|
|
- (void)updatePeerInfoDisplay
|
|
{
|
|
NSString *peerDisplayName;
|
|
NSString *peerAvatarURL;
|
|
|
|
if (self.peer)
|
|
{
|
|
peerDisplayName = [self.peer displayname];
|
|
if (!peerDisplayName.length)
|
|
{
|
|
peerDisplayName = self.peer.userId;
|
|
}
|
|
peerAvatarURL = self.peer.avatarUrl;
|
|
}
|
|
else if (self.mxCall.isConferenceCall)
|
|
{
|
|
peerDisplayName = self.mxCall.room.riotDisplayname;
|
|
peerAvatarURL = self.mxCall.room.state.avatar;
|
|
}
|
|
|
|
self.callerNameLabel.text = peerDisplayName;
|
|
|
|
self.callerImageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
if (peerAvatarURL)
|
|
{
|
|
// Retrieve the avatar in full resolution
|
|
NSString *avatarThumbURL = [self.mainSession.matrixRestClient urlOfContent:peerAvatarURL];
|
|
[self.callerImageView setImageURL:avatarThumbURL withType:nil andImageOrientation:UIImageOrientationUp previewImage:self.picturePlaceholder];
|
|
}
|
|
else
|
|
{
|
|
self.callerImageView.image = self.picturePlaceholder;
|
|
}
|
|
}
|
|
|
|
- (void)showOverlayContainer:(BOOL)isShown
|
|
{
|
|
[super showOverlayContainer:isShown];
|
|
|
|
self.gradientMaskContainerView.hidden = self.overlayContainerView.isHidden;
|
|
}
|
|
|
|
#pragma mark - Sounds
|
|
|
|
- (NSURL*)audioURLWithName:(NSString*)soundName
|
|
{
|
|
NSURL *audioUrl;
|
|
|
|
NSString *path = [[NSBundle mainBundle] pathForResource:soundName ofType:@"mp3"];
|
|
if (path)
|
|
{
|
|
audioUrl = [NSURL fileURLWithPath:path];
|
|
}
|
|
|
|
// Use by default the matrix kit sounds.
|
|
if (!audioUrl)
|
|
{
|
|
audioUrl = [super audioURLWithName:soundName];
|
|
}
|
|
|
|
return audioUrl;
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (IBAction)onButtonPressed:(id)sender
|
|
{
|
|
if (sender == _chatButton)
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
// Dismiss the view controller whereas the call is still running
|
|
[self.delegate dismissCallViewController:self completion:^{
|
|
|
|
if (self.mxCall.room)
|
|
{
|
|
// Open the room page
|
|
[[AppDelegate theDelegate] showRoom:self.mxCall.room.state.roomId andEventId:nil withMatrixSession:self.mxCall.room.mxSession];
|
|
}
|
|
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[super onButtonPressed:sender];
|
|
}
|
|
}
|
|
|
|
@end
|