import UIKit import UserNotifications import UserNotificationsUI import Display import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData import TelegramUIPreferences import AccountContext import Tuples import StickerResources import PhotoResources import AnimatedStickerNode import TelegramAnimatedStickerNode import OpenSSLEncryptionProvider import AppLock private enum NotificationContentAuthorizationError { case unauthorized } private var sharedAccountContext: SharedAccountContext? private var installedSharedLogger = false private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } public struct NotificationViewControllerInitializationData { public let appBundleId: String public let appBuildType: TelegramAppBuildType public let appGroupPath: String public let apiId: Int32 public let apiHash: String public let languagesCategory: String public let encryptionParameters: (Data, Data) public let appVersion: String public let bundleData: Data? public let useBetaFeatures: Bool public init(appBundleId: String, appBuildType: TelegramAppBuildType, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?, useBetaFeatures: Bool) { self.appBundleId = appBundleId self.appBuildType = appBuildType self.appGroupPath = appGroupPath self.apiId = apiId self.apiHash = apiHash self.languagesCategory = languagesCategory self.encryptionParameters = encryptionParameters self.appVersion = appVersion self.bundleData = bundleData self.useBetaFeatures = useBetaFeatures } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) public final class NotificationViewControllerImpl { private let initializationData: NotificationViewControllerInitializationData private let setPreferredContentSize: (CGSize) -> Void private let imageNode = TransformImageNode() private var animatedStickerNode: AnimatedStickerNode? private var imageInfo: (isSticker: Bool, dimensions: CGSize)? private let applyDisposable = MetaDisposable() private let fetchedDisposable = MetaDisposable() private var accountsPath: String? public init(initializationData: NotificationViewControllerInitializationData, setPreferredContentSize: @escaping (CGSize) -> Void) { self.initializationData = initializationData self.setPreferredContentSize = setPreferredContentSize } deinit { self.applyDisposable.dispose() self.fetchedDisposable.dispose() } public func viewDidLoad(view: UIView) { view.addSubnode(self.imageNode) let rootPath = rootPathForBasePath(self.initializationData.appGroupPath) performAppGroupUpgrades(appGroupPath: self.initializationData.appGroupPath, rootPath: rootPath) TempBox.initializeShared(basePath: rootPath, processType: "notification-content", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/logs/notificationcontent-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) setupSharedLogger(rootPath: rootPath, path: logsPath) accountsPath = rootPath if sharedAccountContext == nil { initializeAccountManagement() let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) var initialPresentationDataAndSettings: InitialPresentationDataAndSettings? let semaphore = DispatchSemaphore(value: 0) let _ = currentPresentationDataAndSettings(accountManager: accountManager, systemUserInterfaceStyle: .light).start(next: { value in initialPresentationDataAndSettings = value semaphore.signal() }) semaphore.wait() let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, appBuildType: self.initializationData.appBuildType, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tgapp", openUrl: { _ in }, openUniversalUrl: { _, completion in completion.completion(false) return }, canOpenUrl: { _ in return false }, getTopWindow: { return nil }, displayNotification: { _ in }, applicationInForeground: .single(false), applicationIsActive: .single(false), clearMessageNotifications: { _ in }, pushIdleTimerExtension: { return EmptyDisposable }, openSettings: {}, openAppStorePage: {}, openSubscriptions: {}, registerForNotifications: { _ in }, requestSiriAuthorization: { _ in }, siriAuthorization: { return .notDetermined }, getWindowHost: { return nil }, presentNativeController: { _ in }, dismissNativeController: { }, getAvailableAlternateIcons: { return [] }, getAlternateIconName: { return nil }, requestSetAlternateIconName: { _, f in f(false) }, forceOrientation: { _ in }) let presentationDataPromise = Promise() let appLockContext = AppLockContextImpl(rootPath: rootPath, window: nil, rootController: nil, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { return nil }) sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil) presentationDataPromise.set(sharedAccountContext!.presentationData) } } public func didReceive(_ notification: UNNotification, view: UIView) { guard let accountsPath = self.accountsPath else { return } guard let accountIdValue = notification.request.content.userInfo["accountId"] as? Int64 else { return } guard let peerIdValue = notification.request.content.userInfo["peerId"] as? Int64 else { return } guard let messageIdNamespace = notification.request.content.userInfo["messageId.namespace"] as? Int32 else { return } guard let messageIdId = notification.request.content.userInfo["messageId.id"] as? Int32 else { return } guard let mediaDataString = notification.request.content.userInfo["media"] as? String else { return } guard let mediaData = Data(base64Encoded: mediaDataString) else { return } guard let media = parseMediaData(data: mediaData) else { return } let messageId = MessageId(peerId: PeerId(peerIdValue), namespace: messageIdNamespace, id: messageIdId) if let image = media as? TelegramMediaImage, let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 120, height: 120)), let largestRepresentation = largestImageRepresentation(image.representations) { let dimensions = largestRepresentation.dimensions let fittedSize = dimensions.cgSize.fitted(CGSize(width: view.bounds.width, height: 1000.0)) view.frame = CGRect(origin: view.frame.origin, size: fittedSize) self.setPreferredContentSize(fittedSize) self.imageInfo = (false, dimensions.cgSize) self.updateImageLayout(boundingSize: view.bounds.size) let mediaBoxPath = accountsPath + "/" + accountRecordIdPathName(AccountRecordId(rawValue: accountIdValue)) + "/postbox/media" if let data = try? Data(contentsOf: URL(fileURLWithPath: mediaBoxPath + "/\(largestRepresentation.resource.id.stringRepresentation)"), options: .mappedRead) { self.imageNode.setSignal(chatMessagePhotoInternal(photoData: .single(Tuple(nil, data, .full, true))) |> map { $0.2 }) return } if let data = try? Data(contentsOf: URL(fileURLWithPath: mediaBoxPath + "/\(thumbnailRepresentation.resource.id.stringRepresentation)"), options: .mappedRead) { self.imageNode.setSignal(chatMessagePhotoInternal(photoData: .single(Tuple(data, nil, .medium, false))) |> map { $0.2 }) } guard let sharedAccountContext = sharedAccountContext else { return } self.applyDisposable.set((sharedAccountContext.activeAccountContexts |> map { _, accounts, _ -> Account? in return accounts.first(where: { $0.0 == AccountRecordId(rawValue: accountIdValue) })?.1.account } |> filter { account in return account != nil } |> take(1) |> mapToSignal { account -> Signal<(Account, ImageMediaReference?), NoError> in guard let account = account else { return .complete() } return account.postbox.messageAtId(messageId) |> take(1) |> map { message in var imageReference: ImageMediaReference? if let message = message { for media in message.media { if let image = media as? TelegramMediaImage { imageReference = .message(message: MessageReference(message), media: image) } } } else { imageReference = .standalone(media: image) } return (account, imageReference) } } |> deliverOnMainQueue).start(next: { [weak self] accountAndImage in guard let strongSelf = self else { return } if let imageReference = accountAndImage.1 { strongSelf.imageNode.setSignal(chatMessagePhoto(postbox: accountAndImage.0.postbox, userLocation: .other, photoReference: imageReference)) accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) strongSelf.fetchedDisposable.set(standaloneChatMessagePhotoInteractiveFetched(account: accountAndImage.0, userLocation: .other, photoReference: imageReference).start()) } })) } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { guard let sharedAccountContext = sharedAccountContext else { return } let fittedSize = dimensions.cgSize.fitted(CGSize(width: min(256.0, view.bounds.width), height: 256.0)) view.frame = CGRect(origin: view.frame.origin, size: fittedSize) self.setPreferredContentSize(fittedSize) self.imageInfo = (true, dimensions.cgSize) self.updateImageLayout(boundingSize: view.bounds.size) self.applyDisposable.set((sharedAccountContext.activeAccountContexts |> map { _, contexts, _ -> AccountContext? in return contexts.first(where: { $0.0 == AccountRecordId(rawValue: accountIdValue) })?.1 } |> filter { context in return context != nil } |> take(1) |> mapToSignal { context -> Signal<(Account, FileMediaReference?), NoError> in guard let account = context?.account else { return .complete() } return account.postbox.messageAtId(messageId) |> take(1) |> map { message in var fileReference: FileMediaReference? if let message = message { for media in message.media { if let file = media as? TelegramMediaFile { fileReference = .message(message: MessageReference(message), media: file) } } } else { fileReference = .standalone(media: file) } return (account, fileReference) } } |> deliverOnMainQueue).start(next: { [weak self, weak view] accountAndImage in guard let strongSelf = self else { return } if let fileReference = accountAndImage.1 { if file.isAnimatedSticker || file.isVideoSticker { let animatedStickerNode: AnimatedStickerNode if let current = strongSelf.animatedStickerNode { animatedStickerNode = current } else { animatedStickerNode = DefaultAnimatedStickerNodeImpl() strongSelf.animatedStickerNode = animatedStickerNode animatedStickerNode.started = { guard let strongSelf = self else { return } strongSelf.imageNode.isHidden = true } if !strongSelf.imageNode.frame.width.isZero { animatedStickerNode.frame = strongSelf.imageNode.frame animatedStickerNode.updateLayout(size: strongSelf.imageNode.frame.size) } view?.addSubnode(animatedStickerNode) } let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0)) if file.isVideoSticker { strongSelf.imageNode.setSignal(chatMessageSticker(postbox: accountAndImage.0.postbox, userLocation: .other, file: fileReference.media, small: false)) } else { strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, userLocation: .other, file: fileReference.media, small: false, size: fittedDimensions)) } animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) animatedStickerNode.visibility = true accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, userLocation: .other, fileReference: fileReference).start()) } else if file.isSticker { if let animatedStickerNode = strongSelf.animatedStickerNode { animatedStickerNode.removeFromSupernode() strongSelf.animatedStickerNode = nil } strongSelf.imageNode.isHidden = false strongSelf.imageNode.setSignal(chatMessageSticker(account: accountAndImage.0, userLocation: .other, file: file, small: false)) accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, userLocation: .other, fileReference: fileReference).start()) } } })) } } public func viewWillTransition(to size: CGSize) { self.updateImageLayout(boundingSize: size) } private func updateImageLayout(boundingSize: CGSize) { if let (isSticker, dimensions) = self.imageInfo { let makeLayout = self.imageNode.asyncLayout() let fittedSize: CGSize if isSticker { fittedSize = dimensions.fitted(CGSize(width: min(256.0, boundingSize.width), height: 256.0)) } else { fittedSize = dimensions.fitted(CGSize(width: boundingSize.width, height: 1000.0)) } let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 0.0), imageSize: fittedSize, boundingSize: fittedSize, intrinsicInsets: UIEdgeInsets())) apply() let displaySize = isSticker ? fittedSize : boundingSize self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - displaySize.width) / 2.0), y: 0.0), size: displaySize) self.animatedStickerNode?.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - displaySize.width) / 2.0), y: 0.0), size: displaySize) self.animatedStickerNode?.updateLayout(size: displaySize) } } }