Files
iOS/Sources/Extensions/NotificationContent/CameraViewController.swift
Zac West 4d9a530637 Reorganize files in repo, pull out build settings from pbxproj (#1140)
This is somewhat in prep of being able to make the project file generated, but also just organizes things into more concrete directory structures.

This pulls out _all_ of the build settings from the root level, and most from the target level, into xcconfigs.

The new directory structure looks like:

- Sources
  - App
    - (everything from HomeAssistant/)
  - WatchApp
  - Shared
  - MacBridge
  - Extensions
    - Intents
    - NotificationContent
    - NotificationService
    - Share
    - Today
    - Watch
    - Widgets
- Tests
  - App
  - UI
  - Shared

Somewhat intentionally, the file structure under these is not yet standardized/organized.

The project targets are now:

- App
- WatchApp
- Shared-iOS
- Shared-watchOS
- MacBridge
- Tests-App
- Tests-UI
- Tests-Shared
- Extension-Intents
- Extension-NotificationContent
- Extension-NotificationService
- Extension-Share
- Extension-Today
- Extension-Widget
- WatchExtension-Watch

This does not yet clean up resources vs. sources, nor does it handle some of the "it's in Sources/App but it's part of Shared" crossover directory issues.
2020-10-03 00:15:04 -07:00

175 lines
6.4 KiB
Swift

//
// Camera.swift
// NotificationContentExtension
//
// Created by Robert Trencheny on 10/2/18.
// Copyright © 2018 Robbie Trencheny. All rights reserved.
//
import UIKit
import UserNotifications
import UserNotificationsUI
import KeychainAccess
import Shared
import Alamofire
import AVFoundation
import AVKit
import PromiseKit
class CameraViewController: UIViewController, NotificationCategory {
var activeViewController: (UIViewController & CameraStreamHandler)? {
willSet {
activeViewController?.willMove(toParent: nil)
newValue.flatMap { addChild($0) }
}
didSet {
oldValue?.view.removeFromSuperview()
oldValue?.removeFromParent()
if let viewController = activeViewController {
view.addSubview(viewController.view)
viewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
viewController.view.topAnchor.constraint(equalTo: view.topAnchor),
viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
viewController.didMove(toParent: self)
}
}
}
enum CameraError: LocalizedError {
case missingEntityId
case missingAPI
var errorDescription: String? {
switch self {
case .missingEntityId:
return L10n.Extensions.NotificationContent.Error.noEntityId
case .missingAPI:
return HomeAssistantAPI.APIError.notConfigured.localizedDescription
}
}
}
func didReceive(notification: UNNotification, extensionContext: NSExtensionContext?) -> Promise<Void> {
guard let entityId = notification.request.content.userInfo["entity_id"] as? String else {
return .init(error: CameraError.missingEntityId)
}
guard let api = HomeAssistantAPI.authenticatedAPI() else {
return .init(error: CameraError.missingAPI)
}
return firstly {
api.StreamCamera(entityId: entityId)
}.recover { error -> Promise<StreamCameraResponse> in
Current.Log.info("falling back due to no streaming info for \(entityId) due to \(error)")
return .value(StreamCameraResponse(fallbackEntityID: entityId))
}.then { [weak self] (result) -> Promise<Void> in
let controllers = Self.possibleControllers
.compactMap { (controllerClass) -> () -> Promise<UIViewController & CameraStreamHandler> in
return {
do {
return .value(try controllerClass.init(api: api, response: result))
} catch {
return Promise(error: error)
}
}
}
return self?.viewController(from: controllers).asVoid() ?? .value(())
}
}
var mediaPlayPauseButtonType: UNNotificationContentExtensionMediaPlayPauseButtonType {
.overlay
}
var mediaPlayPauseButtonFrame: CGRect? { nil }
func mediaPlay() {
activeViewController?.play()
}
func mediaPause() {
activeViewController?.pause()
}
enum CameraViewControllerError: LocalizedError {
case noControllers
case accumulated([Error])
var errorDescription: String? {
switch self {
case .noControllers:
return nil
case .accumulated(let errors):
return errors.map { $0.localizedDescription }.joined(separator: "\n\n")
}
}
}
private static var possibleControllers: [(UIViewController & CameraStreamHandler).Type] { [
CameraStreamHLSViewController.self,
CameraStreamMJPEGViewController.self
] }
private func viewController(
from controllerPromises: [() -> Promise<UIViewController & CameraStreamHandler>]
) -> Promise<UIViewController & CameraStreamHandler> {
var accumulatedErrors = [Error]()
var promise: Promise<UIViewController & CameraStreamHandler> = .init(error:
CameraViewControllerError.noControllers
)
for nextPromise in controllerPromises {
promise = promise.recover { [extensionContext] error -> Promise<UIViewController & CameraStreamHandler> in
// always tell the extension context the previous one failed, aka go back to showing pause
extensionContext?.mediaPlayingPaused()
// accumulate the error
if case CameraViewControllerError.noControllers = error {
// except the empty one that we started with to make this code nicer
} else {
accumulatedErrors.append(error)
}
return firstly {
// now try this latest one
nextPromise()
}.get { [weak self, extensionContext] controller in
// configure it -- this isn't part of the one-level-up chain because it would run for each one
var lastState: CameraStreamHandlerState?
controller.didUpdateState = { state in
guard lastState != state else {
return
}
switch state {
case .playing:
extensionContext?.mediaPlayingStarted()
case .paused:
extensionContext?.mediaPlayingPaused()
}
lastState = state
}
// add it to hirearchy and constrain
self?.activeViewController = controller
}.then { value in
// make sure we wait until the controller figures out if it started or failed
value.promise.map { value }
}
}
}
return promise.recover { (nextError) -> Promise<UIViewController & CameraStreamHandler> in
throw CameraViewControllerError.accumulated(accumulatedErrors + [nextError])
}
}
}