diff --git a/Signal/src/ViewControllers/Attachment Keyboard/AttachmentFormatPickerView.swift b/Signal/src/ViewControllers/Attachment Keyboard/AttachmentFormatPickerView.swift index 874174d972..7b37ca0a2a 100644 --- a/Signal/src/ViewControllers/Attachment Keyboard/AttachmentFormatPickerView.swift +++ b/Signal/src/ViewControllers/Attachment Keyboard/AttachmentFormatPickerView.swift @@ -5,7 +5,7 @@ import Foundation protocol AttachmentFormatPickerDelegate: class { - func didTapCamera() + func didTapCamera(withPhotoCapture: PhotoCapture?) func didTapGif() func didTapFile() func didTapContact() @@ -20,6 +20,7 @@ class AttachmentFormatPickerView: UICollectionView { } private let collectionViewFlowLayout = UICollectionViewFlowLayout() + private var photoCapture: PhotoCapture? override var bounds: CGRect { didSet { @@ -47,6 +48,30 @@ class AttachmentFormatPickerView: UICollectionView { updateLayout() } + deinit { + stopCameraPreview() + } + + func startCameraPreview() { + guard photoCapture == nil else { return } + + let photoCapture = PhotoCapture() + self.photoCapture = photoCapture + + photoCapture.startVideoCapture().done { [weak self] in + self?.reloadData() + }.retainUntilComplete() + } + + func stopCameraPreview() { + photoCapture?.stopCapture().retainUntilComplete() + photoCapture = nil + } + + func orientationDidChange() { + updateLayout() + } + private func updateLayout() { AssertIsOnMainThread() @@ -77,7 +102,11 @@ extension AttachmentFormatPickerView: UICollectionViewDelegate { public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { switch AttachmentType.allCases[indexPath.row] { case .camera: - attachmentFormatPickerDelegate?.didTapCamera() + // Since we're trying to pass on our prepared capture session to + // the camera view, nil it out so we don't try and stop it here. + let photoCapture = self.photoCapture + self.photoCapture = nil + attachmentFormatPickerDelegate?.didTapCamera(withPhotoCapture: photoCapture) case .contact: attachmentFormatPickerDelegate?.didTapContact() case .file: @@ -108,7 +137,7 @@ extension AttachmentFormatPickerView: UICollectionViewDataSource { } let type = AttachmentType.allCases[indexPath.item] - cell.configure(type: type) + cell.configure(type: type, cameraPreview: photoCapture?.previewView) return cell } } @@ -121,6 +150,11 @@ class AttachmentFormatCell: UICollectionViewCell { let label = UILabel() var attachmentType: AttachmentType? + weak var cameraPreview: CapturePreviewView? + + private var hasCameraAccess: Bool { + return AVCaptureDevice.authorizationStatus(for: .video) == .authorized + } override init(frame: CGRect) { @@ -164,8 +198,9 @@ class AttachmentFormatCell: UICollectionViewCell { notImplemented() } - public func configure(type: AttachmentType) { + public func configure(type: AttachmentType, cameraPreview: CapturePreviewView?) { self.attachmentType = type + self.cameraPreview = cameraPreview let imageName: String let text: String @@ -196,6 +231,25 @@ class AttachmentFormatCell: UICollectionViewCell { } label.text = text + + showLiveCameraIfAvailable() + } + + func showLiveCameraIfAvailable() { + guard case .camera? = attachmentType, hasCameraAccess else { return } + + // If we have access to the camera, we'll want to show it eventually. + // Style this in prepration for that. + + imageView.setTemplateImageName("camera-outline-32", tintColor: .white) + label.textColor = .white + backgroundColor = UIColor.black.withAlphaComponent(0.4) + + guard let cameraPreview = cameraPreview else { return } + + contentView.insertSubview(cameraPreview, belowSubview: imageView) + cameraPreview.autoPinEdgesToSuperviewEdges() + cameraPreview.contentMode = .scaleAspectFill } override public func prepareForReuse() { @@ -203,5 +257,12 @@ class AttachmentFormatCell: UICollectionViewCell { attachmentType = nil imageView.image = nil + + label.textColor = Theme.attachmentKeyboardItemImageColor + backgroundColor = Theme.attachmentKeyboardItemBackgroundColor + + if let cameraPreview = cameraPreview, cameraPreview.superview == contentView { + cameraPreview.removeFromSuperview() + } } } diff --git a/Signal/src/ViewControllers/Attachment Keyboard/AttachmentKeyboard.swift b/Signal/src/ViewControllers/Attachment Keyboard/AttachmentKeyboard.swift index 0fa2523bd8..96c16f4308 100644 --- a/Signal/src/ViewControllers/Attachment Keyboard/AttachmentKeyboard.swift +++ b/Signal/src/ViewControllers/Attachment Keyboard/AttachmentKeyboard.swift @@ -10,7 +10,7 @@ import PromiseKit protocol AttachmentKeyboardDelegate { func didSelectRecentPhoto(_ attachment: SignalAttachment) func didTapGalleryButton() - func didTapCamera() + func didTapCamera(withPhotoCapture: PhotoCapture?) func didTapGif() func didTapFile() func didTapContact() @@ -147,6 +147,19 @@ class AttachmentKeyboard: CustomKeyboard { checkPermissionsAndLayout() } + override func wasDismissed() { + super.wasDismissed() + + attachmentFormatPickerView.stopCameraPreview() + } + + override func orientationDidChange() { + super.orientationDidChange() + + recentPhotosCollectionView.orientationDidChange() + attachmentFormatPickerView.orientationDidChange() + } + func checkPermissionsAndLayout() { switch mediaLibraryAuthorizationStatus { case .authorized: @@ -154,9 +167,9 @@ class AttachmentKeyboard: CustomKeyboard { case .denied, .restricted: showRecentPhotosError() case .notDetermined: - PHPhotoLibrary.requestAuthorization { _ in + PHPhotoLibrary.requestAuthorization { status in DispatchQueue.main.async { - self.recentPhotosCollectionView.permissionChanged() + if status == .authorized { self.recentPhotosCollectionView.permissionChanged() } self.checkPermissionsAndLayout() } } @@ -164,6 +177,21 @@ class AttachmentKeyboard: CustomKeyboard { showRecentPhotosError() break } + + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + attachmentFormatPickerView.startCameraPreview() + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { granted in + if granted { + DispatchQueue.main.async { self.attachmentFormatPickerView.startCameraPreview() } + } + } + case .denied, .restricted: + break + @unknown default: + break + } } } @@ -178,8 +206,8 @@ extension AttachmentKeyboard: RecentPhotosDelegate { } extension AttachmentKeyboard: AttachmentFormatPickerDelegate { - func didTapCamera() { - delegate?.didTapCamera() + func didTapCamera(withPhotoCapture photoCapture: PhotoCapture?) { + delegate?.didTapCamera(withPhotoCapture: photoCapture) } func didTapGif() { diff --git a/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift b/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift index bfc2a13dfd..4a4966241a 100644 --- a/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift +++ b/Signal/src/ViewControllers/Attachment Keyboard/RecentPhotoCollectionView.swift @@ -88,6 +88,10 @@ class RecentPhotosCollectionView: UICollectionView { updateLayout() } + func orientationDidChange() { + updateLayout() + } + private func updateLayout() { AssertIsOnMainThread() diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index de5f134de5..9e429fb02f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN @class ConversationStyle; @class OWSLinkPreviewDraft; @class OWSQuotedReplyModel; +@class PhotoCapture; @class SignalAttachment; @class StickerInfo; @@ -34,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)cameraButtonPressed; +- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture; + - (void)galleryButtonPressed; - (void)gifButtonPressed; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index cbd82d2ccc..fb6e39ae37 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -504,11 +504,6 @@ const CGFloat kMaxTextViewHeight = 98; { if (!self.desiredFirstResponder.isFirstResponder) { [self.desiredFirstResponder becomeFirstResponder]; - - if ([self.desiredFirstResponder isKindOfClass:[OWSCustomKeyboard class]]) { - OWSCustomKeyboard *customKeyboard = (OWSCustomKeyboard *)self.desiredFirstResponder; - [customKeyboard wasPresented]; - } } } @@ -1532,9 +1527,9 @@ const CGFloat kMaxTextViewHeight = 98; [self.inputToolbarDelegate galleryButtonPressed]; } -- (void)didTapCamera +- (void)didTapCameraWithPhotoCapture:(nullable PhotoCapture *)photoCapture { - [self.inputToolbarDelegate cameraButtonPressed]; + [self.inputToolbarDelegate cameraButtonPressedWithPhotoCapture:photoCapture]; } - (void)didTapGif diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index c470a71851..71ec50fe9b 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2867,7 +2867,7 @@ typedef enum : NSUInteger { #pragma mark - Media Libary -- (void)takePictureOrVideo +- (void)takePictureOrVideoWithPhotoCapture:(nullable PhotoCapture *)photoCapture { [BenchManager startEventWithTitle:@"Show-Camera" eventId:@"Show-Camera"]; [self ows_askForCameraPermissions:^(BOOL cameraGranted) { @@ -2882,7 +2882,8 @@ typedef enum : NSUInteger { // be silent. } - SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingCameraFirst]; + SendMediaNavigationController *pickerModal = + [SendMediaNavigationController showingCameraFirstWithPhotoCapture:photoCapture]; pickerModal.sendMediaNavDelegate = self; [self dismissKeyBoard]; @@ -3215,7 +3216,14 @@ typedef enum : NSUInteger { { OWSAssertIsOnMainThread(); - [self takePictureOrVideo]; + [self takePictureOrVideoWithPhotoCapture:nil]; +} + +- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture +{ + OWSAssertIsOnMainThread(); + + [self takePictureOrVideoWithPhotoCapture:photoCapture]; } - (void)galleryButtonPressed diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index 8274f3212f..bb63ae56a4 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -30,6 +30,7 @@ protocol PhotoCaptureDelegate: AnyObject { func endCaptureButtonAnimation(_ duration: TimeInterval) } +@objc class PhotoCapture: NSObject { weak var delegate: PhotoCaptureDelegate? @@ -38,6 +39,9 @@ class PhotoCapture: NSObject { } let session: AVCaptureSession + // There can only ever be one `CapturePreviewView` per AVCaptureSession + lazy private(set) var previewView = CapturePreviewView(session: session) + let sessionQueue = DispatchQueue(label: "PhotoCapture.sessionQueue") private var currentCaptureInput: AVCaptureDeviceInput? @@ -101,6 +105,9 @@ class PhotoCapture: NSObject { } func startVideoCapture() -> Promise { + // If the session is already running, no need to do anything. + guard !self.session.isRunning else { return Promise.value(()) } + return sessionQueue.async(.promise) { [weak self] in guard let self = self else { return } @@ -410,11 +417,11 @@ extension PhotoCapture: VolumeButtonObserver { func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { handleLongPressBegin() } - + func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { handleLongPressComplete() } - + func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { handleLongPressCancel() } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index f435b1bdd8..19e12c1206 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -36,15 +36,13 @@ class PhotoCaptureViewController: OWSViewController { weak var delegate: PhotoCaptureViewControllerDelegate? - private var photoCapture: PhotoCapture! + @objc public lazy var photoCapture = PhotoCapture() deinit { UIDevice.current.endGeneratingDeviceOrientationNotifications() - if let photoCapture = photoCapture { - photoCapture.stopCapture().done { - Logger.debug("stopCapture completed") - }.retainUntilComplete() - } + photoCapture.stopCapture().done { + Logger.debug("stopCapture completed") + }.retainUntilComplete() } // MARK: - Overrides @@ -136,7 +134,9 @@ class PhotoCaptureViewController: OWSViewController { // MARK: - Views let captureButton = CaptureButton() - var previewView: CapturePreviewView! + var previewView: CapturePreviewView { + return photoCapture.previewView + } class PhotoControl { let button: OWSButton @@ -315,21 +315,26 @@ class PhotoCaptureViewController: OWSViewController { var hasCaptureStarted = false private func setupPhotoCapture() { - photoCapture = PhotoCapture() photoCapture.delegate = self captureButton.delegate = photoCapture - previewView = CapturePreviewView(session: photoCapture.session) + previewView.contentMode = .scaleAspectFit - photoCapture.startVideoCapture().done { [weak self] in + let captureReady = { [weak self] in guard let self = self else { return } self.hasCaptureStarted = true self.showCaptureUI() BenchEventComplete(eventId: "Show-Camera") - }.catch { [weak self] error in - guard let self = self else { return } + } - self.showFailureUI(error: error) - }.retainUntilComplete() + // If the session is already running, we're good to go. + guard !photoCapture.session.isRunning else { return captureReady() } + + photoCapture.startVideoCapture() + .done(captureReady) + .catch { [weak self] error in + guard let self = self else { return } + self.showFailureUI(error: error) + }.retainUntilComplete() } private func showCaptureUI() { @@ -646,10 +651,38 @@ class CapturePreviewView: UIView { } } + override var contentMode: UIView.ContentMode { + set { + switch newValue { + case .scaleAspectFill: + previewLayer.videoGravity = .resizeAspectFill + case .scaleAspectFit: + previewLayer.videoGravity = .resizeAspect + case .scaleToFill: + previewLayer.videoGravity = .resize + default: + owsFailDebug("Unexpected contentMode") + } + } + get { + switch previewLayer.videoGravity { + case .resizeAspectFill: + return .scaleAspectFill + case .resizeAspect: + return .scaleAspectFit + case .resize: + return .scaleToFill + default: + owsFailDebug("Unexpected contentMode") + return .scaleToFill + } + } + } + init(session: AVCaptureSession) { previewLayer = AVCaptureVideoPreviewLayer(session: session) super.init(frame: .zero) - self.contentMode = .scaleAspectFill + self.contentMode = .scaleAspectFit previewLayer.frame = bounds layer.addSublayer(previewLayer) } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 6c7c3c7b09..69b592ec85 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -106,8 +106,11 @@ class SendMediaNavigationController: OWSNavigationController { public weak var sendMediaNavDelegate: SendMediaNavDelegate? @objc - public class func showingCameraFirst() -> SendMediaNavigationController { + public class func showingCameraFirst(photoCapture: PhotoCapture?) -> SendMediaNavigationController { let navController = SendMediaNavigationController() + if let photoCapture = photoCapture { + navController.captureViewController.photoCapture = photoCapture + } navController.setViewControllers([navController.captureViewController], animated: false) return navController } diff --git a/SignalMessaging/Views/CustomKeyboard.swift b/SignalMessaging/Views/CustomKeyboard.swift index 929455fb71..319ac156a5 100644 --- a/SignalMessaging/Views/CustomKeyboard.swift +++ b/SignalMessaging/Views/CustomKeyboard.swift @@ -31,7 +31,8 @@ open class CustomKeyboard: UIInputView { fatalError("init(coder:) has not been implemented") } - @objc open func wasPresented() {} + open func wasPresented() {} + open func wasDismissed() {} @objc public func registerWithView(_ view: UIView) { view.addSubview(responder) @@ -48,6 +49,14 @@ open class CustomKeyboard: UIInputView { return responder.resignFirstResponder() } + open override func didMoveToSuperview() { + if superview == nil { + wasDismissed() + } else { + wasPresented() + } + } + // MARK: - Height Management private lazy var heightConstraint = autoSetDimension(.height, toSize: 0) @@ -107,7 +116,7 @@ open class CustomKeyboard: UIInputView { } private class CustomKeyboardResponder: UIView { - @objc public let customKeyboard: CustomKeyboard + @objc public weak var customKeyboard: CustomKeyboard? init(customKeyboard: CustomKeyboard) { self.customKeyboard = customKeyboard