Ilya Laktyushin e5762bd9c8 Various fixes
2025-05-18 04:35:56 +04:00

3240 lines
158 KiB
Swift

import UIKit
import SwiftSignalKit
import Display
import TelegramCore
import UserNotifications
import Intents
import Postbox
import PushKit
import AsyncDisplayKit
import TelegramUIPreferences
import TelegramPresentationData
import TelegramCallsUI
import TelegramVoip
import BuildConfig
import BuildConfigExtra
import DeviceCheck
import AccountContext
import OverlayStatusController
import UndoUI
import LegacyUI
import PassportUI
import WatchBridge
import SettingsUI
import AppBundle
import UrlHandling
import OpenSSLEncryptionProvider
import AppLock
import PresentationDataUtils
import TelegramIntents
import AccountUtils
import CoreSpotlight
import TelegramAudio
import DebugSettingsUI
import BackgroundTasks
import UIKitRuntimeUtils
import StoreKit
import PhoneNumberFormat
import AuthorizationUI
import ManagedFile
import DeviceProximity
import MediaEditor
import TelegramUIDeclareEncodables
import ContextMenuScreen
import MetalEngine
import RecaptchaEnterprise
#if canImport(AppCenter)
import AppCenter
import AppCenterCrashes
#endif
private let handleVoipNotifications = false
private var testIsLaunched = false
private func isKeyboardWindow(window: NSObject) -> Bool {
let typeName = NSStringFromClass(type(of: window))
if #available(iOS 9.0, *) {
if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") {
return true
}
} else {
if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") {
return true
}
}
return false
}
private func isKeyboardView(view: NSObject) -> Bool {
let typeName = NSStringFromClass(type(of: view))
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") {
return true
}
return false
}
private func isKeyboardViewContainer(view: NSObject) -> Bool {
let typeName = NSStringFromClass(type(of: view))
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") {
return true
}
return false
}
private class ApplicationStatusBarHost: StatusBarHost {
private let application = UIApplication.shared
var isApplicationInForeground: Bool {
switch self.application.applicationState {
case .background:
return false
default:
return true
}
}
var statusBarFrame: CGRect {
return self.application.statusBarFrame
}
var statusBarStyle: UIStatusBarStyle {
get {
return self.application.statusBarStyle
} set(value) {
self.setStatusBarStyle(value, animated: false)
}
}
func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) {
if self.shouldChangeStatusBarStyle?(style) ?? true {
self.application.internalSetStatusBarStyle(style, animated: animated)
}
}
var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)?
func setStatusBarHidden(_ value: Bool, animated: Bool) {
self.application.internalSetStatusBarHidden(value, animation: animated ? .fade : .none)
}
var keyboardWindow: UIWindow? {
if #available(iOS 16.0, *) {
return UIApplication.shared.internalGetKeyboard()
}
for window in UIApplication.shared.windows {
if isKeyboardWindow(window: window) {
return window
}
}
return nil
}
var keyboardView: UIView? {
guard let keyboardWindow = self.keyboardWindow else {
return nil
}
for view in keyboardWindow.subviews {
if isKeyboardViewContainer(view: view) {
for subview in view.subviews {
if isKeyboardView(view: subview) {
return subview
}
}
}
}
return nil
}
}
private func legacyDocumentsPath() -> String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/legacy"
}
protocol SupportedStartCallIntent {
@available(iOS 10.0, *)
var contacts: [INPerson]? { get }
}
@available(iOS 10.0, *)
extension INStartAudioCallIntent: SupportedStartCallIntent {}
protocol SupportedStartVideoCallIntent {
@available(iOS 10.0, *)
var contacts: [INPerson]? { get }
}
@available(iOS 10.0, *)
extension INStartVideoCallIntent: SupportedStartVideoCallIntent {}
private enum QueuedWakeup: Int32 {
case call
case backgroundLocation
}
final class SharedApplicationContext {
let sharedContext: SharedAccountContextImpl
let notificationManager: SharedNotificationManager
let wakeupManager: SharedWakeupManager
let overlayMediaController: ViewController & OverlayMediaController
var minimizedContainer: [AccountRecordId: MinimizedContainer] = [:]
init(sharedContext: SharedAccountContextImpl, notificationManager: SharedNotificationManager, wakeupManager: SharedWakeupManager) {
self.sharedContext = sharedContext
self.notificationManager = notificationManager
self.wakeupManager = wakeupManager
self.overlayMediaController = OverlayMediaControllerImpl()
}
}
private struct AccountManagerState {
struct NotificationKey {
var accountId: AccountRecordId
var id: Data
var key: Data
}
var notificationKeys: [NotificationKey]
}
private func extractAccountManagerState(records: AccountRecordsView<TelegramAccountManagerTypes>) -> AccountManagerState {
return AccountManagerState(
notificationKeys: records.records.compactMap { record -> AccountManagerState.NotificationKey? in
for attribute in record.attributes {
if case let .backupData(backupData) = attribute {
if let notificationEncryptionKeyId = backupData.data?.notificationEncryptionKeyId, let notificationEncryptionKey = backupData.data?.notificationEncryptionKey {
return AccountManagerState.NotificationKey(
accountId: record.id,
id: notificationEncryptionKeyId,
key: notificationEncryptionKey
)
}
}
}
return nil
}
)
}
@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate, URLSessionDelegate, URLSessionTaskDelegate {
@objc var window: UIWindow?
var nativeWindow: (UIWindow & WindowHost)?
var mainWindow: Window1!
private var dataImportSplash: LegacyDataImportSplash?
private var memoryUsageOverlayView: UILabel?
private var buildConfig: BuildConfig?
let episodeId = arc4random()
private let isInForegroundPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private var isInForegroundValue = false
private let isActivePromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private var isActiveValue = false
let hasActiveAudioSession = Promise<Bool>(false)
private let sharedContextPromise = Promise<SharedApplicationContext>()
//private let watchCommunicationManagerPromise = Promise<WatchCommunicationManager?>()
private var accountManager: AccountManager<TelegramAccountManagerTypes>?
private var accountManagerState: AccountManagerState?
private var contextValue: AuthorizedApplicationContext?
private let context = Promise<AuthorizedApplicationContext?>()
private let contextDisposable = MetaDisposable()
private var authContextValue: UnauthorizedApplicationContext?
private let authContext = Promise<UnauthorizedApplicationContext?>()
private let authContextDisposable = MetaDisposable()
private let logoutDisposable = MetaDisposable()
private let openNotificationSettingsWhenReadyDisposable = MetaDisposable()
private let openChatWhenReadyDisposable = MetaDisposable()
private let openUrlWhenReadyDisposable = MetaDisposable()
private let badgeDisposable = MetaDisposable()
private let quickActionsDisposable = MetaDisposable()
private var pushRegistry: PKPushRegistry?
private let notificationAuthorizationDisposable = MetaDisposable()
private var replyFromNotificationsDisposables = DisposableSet()
private var watchedCallsDisposables = DisposableSet()
private var _notificationTokenPromise: Promise<Data>?
private let voipTokenPromise = Promise<Data>()
private var firebaseSecrets: [String: String] = [:] {
didSet {
if self.firebaseSecrets != oldValue {
self.firebaseSecretStream.set(.single(self.firebaseSecrets))
}
}
}
private let firebaseSecretStream = Promise<[String: String]>([:])
private var firebaseRequestVerificationSecrets: [String: String] = [:] {
didSet {
if self.firebaseRequestVerificationSecrets != oldValue {
self.firebaseRequestVerificationSecretStream.set(.single(self.firebaseRequestVerificationSecrets))
}
}
}
private let firebaseRequestVerificationSecretStream = Promise<[String: String]>([:])
private var urlSessions: [URLSession] = []
private func urlSession(identifier: String) -> URLSession {
if let existingSession = self.urlSessions.first(where: { $0.configuration.identifier == identifier }) {
return existingSession
}
let baseAppBundleId = Bundle.main.bundleIdentifier!
let appGroupName = "group.\(baseAppBundleId)"
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
configuration.sharedContainerIdentifier = appGroupName
configuration.isDiscretionary = false
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
self.urlSessions.append(session)
return session
}
private var pendingUrlSessionBackgroundEventsCompletion: (() -> Void)?
private var notificationTokenPromise: Promise<Data> {
if let current = self._notificationTokenPromise {
return current
} else {
let promise = Promise<Data>()
self._notificationTokenPromise = promise
return promise
}
}
private var clearNotificationsManager: ClearNotificationsManager?
private let idleTimerExtensionSubscribers = Bag<Void>()
private var alertActions: (primary: (() -> Void)?, other: (() -> Void)?)?
private let voipDeviceToken = Promise<Data?>(nil)
private let regularDeviceToken = Promise<Data?>(nil)
private var recaptchaClientsBySiteKey: [String: Promise<RecaptchaClient>] = [:]
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
precondition(!testIsLaunched)
testIsLaunched = true
let _ = voipTokenPromise.get().start(next: { token in
self.voipDeviceToken.set(.single(token))
})
let _ = notificationTokenPromise.get().start(next: { token in
self.regularDeviceToken.set(.single(token))
})
let launchStartTime = CFAbsoluteTimeGetCurrent()
let statusBarHost = ApplicationStatusBarHost()
let (window, hostView) = nativeWindowHostView()
self.mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost)
if let traitCollection = window.rootViewController?.traitCollection {
if #available(iOS 13.0, *) {
switch traitCollection.userInterfaceStyle {
case .light, .unspecified:
hostView.containerView.backgroundColor = UIColor.white
default:
hostView.containerView.backgroundColor = UIColor.black
}
} else {
hostView.containerView.backgroundColor = UIColor.white
}
} else {
hostView.containerView.backgroundColor = UIColor.white
}
self.window = window
self.nativeWindow = window
hostView.containerView.layer.addSublayer(MetalEngine.shared.rootLayer)
if !UIDevice.current.isBatteryMonitoringEnabled {
UIDevice.current.isBatteryMonitoringEnabled = true
}
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
var result: [(String, NotificationManagedNotificationRequestId)] = []
for notification in notifications {
if let requestId = NotificationManagedNotificationRequestId(string: notification.request.identifier) {
result.append((notification.request.identifier, requestId))
} else {
let payload = notification.request.content.userInfo
var notificationRequestId: NotificationManagedNotificationRequestId?
var peerId: PeerId?
if let fromId = payload["from_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
} else if let fromId = payload["chat_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
} else if let fromId = payload["channel_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
}
if let msgId = payload["msg_id"] {
let msgIdValue = msgId as! NSString
if let peerId = peerId {
notificationRequestId = .messageId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue)))
}
}
if let notificationRequestId = notificationRequestId {
result.append((notification.request.identifier, notificationRequestId))
}
}
}
completion.f(result)
})
} else {
var result: [(String, NotificationManagedNotificationRequestId)] = []
if let notifications = UIApplication.shared.scheduledLocalNotifications {
for notification in notifications {
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
if let requestId = NotificationManagedNotificationRequestId(string: id) {
result.append((id, requestId))
}
}
}
}
completion.f(result)
}
}, removeNotificationIds: { ids in
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
} else {
if let notifications = UIApplication.shared.scheduledLocalNotifications {
for notification in notifications {
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
if ids.contains(id) {
UIApplication.shared.cancelLocalNotification(notification)
}
}
}
}
}
}, getPendingNotificationIds: { completion in
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in
var result: [(String, NotificationManagedNotificationRequestId)] = []
for request in requests {
if let requestId = NotificationManagedNotificationRequestId(string: request.identifier) {
result.append((request.identifier, requestId))
}
}
completion.f(result)
})
} else {
var result: [(String, NotificationManagedNotificationRequestId)] = []
if let notifications = UIApplication.shared.scheduledLocalNotifications {
for notification in notifications {
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
if let requestId = NotificationManagedNotificationRequestId(string: id) {
result.append((id, requestId))
}
}
}
}
completion.f(result)
}
}, removePendingNotificationIds: { ids in
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
} else {
if let notifications = UIApplication.shared.scheduledLocalNotifications {
for notification in notifications {
if let userInfo = notification.userInfo, let id = userInfo["id"] as? String {
if ids.contains(id) {
UIApplication.shared.cancelLocalNotification(notification)
}
}
}
}
}
})
self.clearNotificationsManager = clearNotificationsManager
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
let baseAppBundleId = Bundle.main.bundleIdentifier!
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
self.buildConfig = buildConfig
let signatureDict = BuildConfigExtra.signatureDict()
let apiId: Int32 = buildConfig.apiId
let apiHash: String = buildConfig.apiHash
let languagesCategory = "ios"
let autolockDeadine: Signal<Int32?, NoError>
if #available(iOS 10.0, *) {
autolockDeadine = .single(nil)
} else {
autolockDeadine = self.context.get()
|> mapToSignal { context -> Signal<Int32?, NoError> in
guard let context = context else {
return .single(nil)
}
return context.context.sharedContext.appLockContext.autolockDeadline
}
}
let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: PresentationCallManagerImpl.voipMaxLayer, voipVersions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in
CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo)
}, appData: self.regularDeviceToken.get()
|> map { token in
let tokenEnvironment: String
#if DEBUG
tokenEnvironment = "sandbox"
#else
tokenEnvironment = "production"
#endif
let data = buildConfig.bundleData(withAppToken: token, tokenType: "apns", tokenEnvironment: tokenEnvironment, signatureDict: signatureDict)
if let data = data, let _ = String(data: data, encoding: .utf8) {
} else {
Logger.shared.log("data", "can't deserialize")
}
return data
}, externalRequestVerificationStream: self.firebaseRequestVerificationSecretStream.get(), externalRecaptchaRequestVerification: { method, siteKey in
return Signal { subscriber in
let recaptchaClient: Promise<RecaptchaClient>
if let current = self.recaptchaClientsBySiteKey[siteKey] {
recaptchaClient = current
} else {
recaptchaClient = Promise<RecaptchaClient>()
self.recaptchaClientsBySiteKey[siteKey] = recaptchaClient
Recaptcha.fetchClient(withSiteKey: siteKey) { client, error in
Queue.mainQueue().async {
guard let client else {
Logger.shared.log("App \(self.episodeId)", "RecaptchaClient creation error: \(String(describing: error)).")
return
}
recaptchaClient.set(.single(client))
}
}
}
return (recaptchaClient.get()
|> take(1)
|> mapToSignal { recaptchaClient -> Signal<String?, NoError> in
return Signal { subscriber in
var recaptchaAction: RecaptchaAction?
switch method {
case "signup":
recaptchaAction = RecaptchaAction.signup
default:
break
}
guard let recaptchaAction else {
subscriber.putNext(nil)
subscriber.putCompletion()
return EmptyDisposable
}
recaptchaClient.execute(withAction: recaptchaAction) { token, error in
if let token {
subscriber.putNext(token)
Logger.shared.log("App \(self.episodeId)", "RecaptchaClient executed successfully")
} else {
subscriber.putNext(nil)
Logger.shared.log("App \(self.episodeId)", "RecaptchaClient execute error: \(String(describing: error))")
}
subscriber.putCompletion()
}
return ActionDisposable {
}
}
|> runOn(Queue.mainQueue())
}).startStandalone(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
|> runOn(Queue.mainQueue())
}, autolockDeadine: autolockDeadine, encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: buildConfig.isICloudEnabled)
guard let appGroupUrl = maybeAppGroupUrl else {
self.mainWindow?.presentNative(UIAlertController(title: nil, message: "Error 2", preferredStyle: .alert))
return true
}
var isDebugConfiguration = false
#if DEBUG
isDebugConfiguration = true
#endif
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
isDebugConfiguration = true
}
if isDebugConfiguration || buildConfig.isInternalBuild {
LoggingSettings.defaultSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true)
} else {
LoggingSettings.defaultSettings = LoggingSettings(logToFile: false, logToConsole: false, redactSensitiveData: true)
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: Int64.random(in: Int64.min ... Int64.max))
let writeAbilityTestFile = TempBox.shared.tempFile(fileName: "test.bin")
var writeAbilityTestSuccess = true
if let testFile = ManagedFile(queue: nil, path: writeAbilityTestFile.path, mode: .readwrite) {
let bufferSize = 128 * 1024
let randomBuffer = malloc(bufferSize)!
defer {
free(randomBuffer)
}
arc4random_buf(randomBuffer, bufferSize)
var writtenBytes = 0
while writtenBytes < 1024 * 1024 {
let actualBytes = testFile.write(randomBuffer, count: bufferSize)
writtenBytes += actualBytes
if actualBytes != bufferSize {
writeAbilityTestSuccess = false
break
}
}
testFile._unsafeClose()
TempBox.shared.dispose(writeAbilityTestFile)
} else {
writeAbilityTestSuccess = false
}
if !writeAbilityTestSuccess {
let alertController = UIAlertController(title: nil, message: "The device does not have sufficient free space.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
preconditionFailure()
}))
self.mainWindow?.presentNative(alertController)
return true
}
let legacyLogs: [String] = [
"broadcast-logs",
"siri-logs",
"widget-logs",
"notificationcontent-logs",
"notification-logs"
]
for item in legacyLogs {
let _ = try? FileManager.default.removeItem(atPath: "\(rootPath)/\(item)")
}
let logsPath = rootPath + "/logs/app-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: logsPath))
setManagedAudioSessionLogger({ s in
Logger.shared.log("ManagedAudioSession", s)
Logger.shared.shortLog("ManagedAudioSession", s)
})
if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath + "/accounts-metadata"), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
for url in contents {
Logger.shared.log("App \(self.episodeId)", "metadata: \(url.path)")
}
}
if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
for url in contents {
Logger.shared.log("App \(self.episodeId)", "root: \(url.path)")
if url.lastPathComponent.hasPrefix("account-") {
if let subcontents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) {
for suburl in subcontents {
Logger.shared.log("App \(self.episodeId)", "account \(url.lastPathComponent): \(suburl.path)")
}
}
}
}
}
//ASDisableLogging()
initializeLegacyComponents(application: application, currentSizeClassGetter: {
return UIUserInterfaceSizeClass.compact
}, currentHorizontalClassGetter: {
return UIUserInterfaceSizeClass.compact
}, documentsPath: legacyDocumentsPath(), currentApplicationBounds: {
return UIScreen.main.bounds
}, canOpenUrl: { url in
return UIApplication.shared.canOpenURL(url)
}, openUrl: { url in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
})
setContextMenuControllerProvider { arguments in
return ContextMenuControllerImpl(arguments)
}
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self
}
GlobalExperimentalSettings.isAppStoreBuild = buildConfig.isAppStoreBuild
GlobalExperimentalSettings.enableFeed = false
self.window?.makeKeyAndVisible()
var hasActiveCalls: Signal<Bool, NoError> = .single(false)
if CallKitIntegration.isAvailable, let callKitIntegration = CallKitIntegration.shared {
hasActiveCalls = callKitIntegration.hasActiveCalls
}
self.hasActiveAudioSession.set(
combineLatest(queue: .mainQueue(),
hasActiveCalls,
MediaManagerImpl.globalAudioSession.isActive()
)
|> map { hasActiveCalls, isActive -> Bool in
return hasActiveCalls || isActive
}
|> distinctUntilChanged
)
let applicationBindings = TelegramApplicationBindings(isMainApp: true, appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in
var parsedUrl = URL(string: url)
if let parsed = parsedUrl {
if parsed.scheme == nil || parsed.scheme!.isEmpty {
parsedUrl = URL(string: "https://\(url)")
}
if parsed.scheme == "tg" {
return
}
}
if let parsedUrl = parsedUrl {
UIApplication.shared.open(parsedUrl, options: [:], completionHandler: nil)
} else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
UIApplication.shared.open(parsedUrl, options: [:], completionHandler: nil)
}
}, openUniversalUrl: { url, completion in
if #available(iOS 10.0, *) {
var parsedUrl = URL(string: url)
if let parsed = parsedUrl {
if parsed.scheme == nil || parsed.scheme!.isEmpty {
parsedUrl = URL(string: "https://\(url)")
}
}
if let parsedUrl = parsedUrl {
return UIApplication.shared.open(parsedUrl, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: true as NSNumber], completionHandler: { value in
completion.completion(value)
})
} else if let escapedUrl = (url.removingPercentEncoding ?? url).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
return UIApplication.shared.open(parsedUrl, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: true as NSNumber], completionHandler: { value in
completion.completion(value)
})
} else {
completion.completion(false)
}
} else {
completion.completion(false)
}
}, canOpenUrl: { url in
var parsedUrl = URL(string: url)
if let parsed = parsedUrl {
if parsed.scheme == nil || parsed.scheme!.isEmpty {
parsedUrl = URL(string: "https://\(url)")
}
}
if let parsedUrl = parsedUrl {
return UIApplication.shared.canOpenURL(parsedUrl)
} else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) {
return UIApplication.shared.canOpenURL(parsedUrl)
} else {
return false
}
}, getTopWindow: {
for window in application.windows.reversed() {
if window === self.window || window === statusBarHost.keyboardWindow {
return window
}
}
return application.windows.last
}, displayNotification: { text in
}, applicationInForeground: self.isInForegroundPromise.get(),
applicationIsActive: self.isActivePromise.get(),
clearMessageNotifications: { ids in
for id in ids {
self.clearNotificationsManager?.append(id)
}
}, pushIdleTimerExtension: {
let disposable = MetaDisposable()
Queue.mainQueue().async {
let wasEmpty = self.idleTimerExtensionSubscribers.isEmpty
let index = self.idleTimerExtensionSubscribers.add(Void())
if wasEmpty {
application.isIdleTimerDisabled = true
}
disposable.set(ActionDisposable {
Queue.mainQueue().async {
self.idleTimerExtensionSubscribers.remove(index)
if self.idleTimerExtensionSubscribers.isEmpty {
application.isIdleTimerDisabled = false
}
}
})
}
return disposable
}, openSettings: {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}, openAppStorePage: {
let appStoreId = buildConfig.appStoreId
if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appStoreId)") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}, openSubscriptions: {
if #available(iOS 15, *), let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
Task {
try await AppStore.showManageSubscriptions(in: scene)
}
} else if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}, registerForNotifications: { completion in
Logger.shared.log("App \(self.episodeId)", "register for notifications begin")
let _ = (self.context.get()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
if let context = context {
Logger.shared.log("App \(self.episodeId)", "register for notifications initiate")
self.registerForNotifications(context: context.context, authorize: true, completion: completion)
}
})
}, requestSiriAuthorization: { completion in
if #available(iOS 10, *) {
INPreferences.requestSiriAuthorization { status in
if case .authorized = status {
completion(true)
} else {
completion(false)
}
}
} else {
completion(false)
}
}, siriAuthorization: {
if buildConfig.isSiriEnabled {
if #available(iOS 10, *) {
switch INPreferences.siriAuthorizationStatus() {
case .authorized:
return .allowed
case .denied, .restricted:
return .denied
case .notDetermined:
return .notDetermined
@unknown default:
return .notDetermined
}
} else {
return .denied
}
} else {
return .denied
}
}, getWindowHost: {
return self.nativeWindow
}, presentNativeController: { controller in
self.window?.rootViewController?.present(controller, animated: true, completion: nil)
}, dismissNativeController: {
self.window?.rootViewController?.dismiss(animated: true, completion: nil)
}, getAvailableAlternateIcons: {
if #available(iOS 10.3, *) {
var icons = [
PresentationAppIcon(name: "BlueIcon", imageName: "BlueIcon", isDefault: buildConfig.isAppStoreBuild),
PresentationAppIcon(name: "New2", imageName: "New2"),
PresentationAppIcon(name: "New1", imageName: "New1"),
PresentationAppIcon(name: "BlackIcon", imageName: "BlackIcon"),
PresentationAppIcon(name: "BlueClassicIcon", imageName: "BlueClassicIcon"),
PresentationAppIcon(name: "BlackClassicIcon", imageName: "BlackClassicIcon"),
PresentationAppIcon(name: "BlueFilledIcon", imageName: "BlueFilledIcon"),
PresentationAppIcon(name: "BlackFilledIcon", imageName: "BlackFilledIcon")
]
if buildConfig.isInternalBuild {
icons.append(PresentationAppIcon(name: "WhiteFilledIcon", imageName: "WhiteFilledIcon"))
}
icons.append(PresentationAppIcon(name: "Premium", imageName: "Premium", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true))
return icons
} else {
return []
}
}, getAlternateIconName: {
if #available(iOS 10.3, *) {
return application.alternateIconName
} else {
return nil
}
}, requestSetAlternateIconName: { name, completion in
if #available(iOS 10.3, *) {
application.setAlternateIconName(name, completionHandler: { error in
if let error = error {
Logger.shared.log("App \(self.episodeId)", "failed to set alternate icon with error \(error.localizedDescription)")
}
completion(error == nil)
})
} else {
completion(false)
}
}, forceOrientation: { orientation in
let value = orientation.rawValue
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
var interfaceOrientations: UIInterfaceOrientationMask = []
switch orientation {
case .portrait:
interfaceOrientations = .portrait
case .landscapeLeft:
interfaceOrientations = .landscapeLeft
case .landscapeRight:
interfaceOrientations = .landscapeRight
case .portraitUpsideDown:
interfaceOrientations = .portraitUpsideDown
case .unknown:
interfaceOrientations = .portrait
@unknown default:
interfaceOrientations = .portrait
}
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: interfaceOrientations))
} else {
UIDevice.current.setValue(value, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
})
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true)
self.accountManager = accountManager
telegramUIDeclareEncodables()
initializeAccountManagement()
let pushRegistry = PKPushRegistry(queue: .main)
if #available(iOS 9.0, *) {
pushRegistry.desiredPushTypes = Set([.voIP])
}
self.pushRegistry = pushRegistry
pushRegistry.delegate = self
self.accountManagerState = extractAccountManagerState(records: accountManager._internalAccountRecordsSync())
let _ = (accountManager.accountRecords()
|> deliverOnMainQueue).start(next: { view in
self.accountManagerState = extractAccountManagerState(records: view)
})
var systemUserInterfaceStyle: WindowUserInterfaceStyle = .light
if #available(iOS 13.0, *) {
if let traitCollection = window.rootViewController?.traitCollection {
systemUserInterfaceStyle = WindowUserInterfaceStyle(style: traitCollection.userInterfaceStyle)
}
}
let sharedContextSignal = currentPresentationDataAndSettings(accountManager: accountManager, systemUserInterfaceStyle: systemUserInterfaceStyle)
|> map { initialPresentationDataAndSettings -> (AccountManager, InitialPresentationDataAndSettings) in
return (accountManager, initialPresentationDataAndSettings)
}
|> deliverOnMainQueue
|> mapToSignal { accountManager, initialPresentationDataAndSettings -> Signal<(SharedApplicationContext, LoggingSettings), NoError> in
self.mainWindow?.hostView.containerView.backgroundColor = initialPresentationDataAndSettings.presentationData.theme.chatList.backgroundColor
let legacyBasePath = appGroupUrl.path
let presentationDataPromise = Promise<PresentationData>()
let appLockContext = AppLockContextImpl(rootPath: rootPath, window: self.mainWindow!, rootController: self.window?.rootViewController, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: {
return (self.mainWindow?.viewController as? TelegramRootController)?.chatListController?.lockViewFrame
})
var setPresentationCall: ((PresentationCall?) -> Void)?
let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in
setPresentationCall?(call)
}, navigateToChat: { accountId, peerId, messageId, alwaysKeepMessageId in
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil, alwaysKeepMessageId: alwaysKeepMessageId)
}, displayUpgradeProgress: { progress in
if let progress = progress {
if self.dataImportSplash == nil {
self.dataImportSplash = makeLegacyDataImportSplash(theme: initialPresentationDataAndSettings.presentationData.theme, strings: initialPresentationDataAndSettings.presentationData.strings)
self.dataImportSplash?.serviceAction = {
self.debugPressed()
}
self.mainWindow.coveringView = self.dataImportSplash
}
self.dataImportSplash?.progress = (.generic, progress)
} else if let dataImportSplash = self.dataImportSplash {
self.dataImportSplash = nil
if self.mainWindow.coveringView === dataImportSplash {
self.mainWindow.coveringView = nil
}
}
}, appDelegate: self)
presentationDataPromise.set(sharedContext.presentationData)
sharedContext.presentGlobalController = { [weak self] c, a in
guard let strongSelf = self else {
return
}
strongSelf.mainWindow.present(c, on: .root)
}
sharedContext.presentCrossfadeController = { [weak self] in
guard let strongSelf = self else {
return
}
var exists = false
strongSelf.mainWindow.forEachViewController({ controller in
if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController || controller is ThemePreviewController {
exists = true
}
return true
})
if !exists {
strongSelf.mainWindow.present(ThemeSettingsCrossfadeController(), on: .root)
}
}
let notificationManager = SharedNotificationManager(episodeId: self.episodeId, application: application, clearNotificationsManager: clearNotificationsManager, inForeground: applicationBindings.applicationInForeground, accounts: sharedContext.activeAccountContexts |> map { primary, accounts, _ in accounts.map({ ($0.1.account, $0.1.account.id == primary?.account.id) }) }, pollLiveLocationOnce: { accountId in
let _ = (self.context.get()
|> filter {
return $0 != nil
}
|> take(1)
|> deliverOnMainQueue).start(next: { context in
if let context = context, context.context.account.id == accountId {
context.context.liveLocationManager?.pollOnce()
}
})
})
setPresentationCall = { call in
notificationManager.setNotificationCall(call, strings: sharedContext.currentPresentationData.with({ $0 }).strings)
}
let liveLocationPolling = self.context.get()
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
if let context = context, let liveLocationManager = context.context.liveLocationManager {
let accountId = context.context.account.id
return combineLatest(queue: .mainQueue(),
liveLocationManager.isPolling,
liveLocationManager.hasBackgroundTasks
)
|> map { isPolling, hasBackgroundTasks -> Bool in
return isPolling || hasBackgroundTasks
}
|> distinctUntilChanged
|> map { value -> AccountRecordId? in
if value {
return accountId
} else {
return nil
}
}
} else {
return .single(nil)
}
}
/*let watchTasks = self.context.get()
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
if let context = context, let watchManager = context.context.watchManager {
let accountId = context.context.account.id
let runningTasks: Signal<WatchRunningTasks?, NoError> = .single(nil)
|> then(watchManager.runningTasks)
return runningTasks
|> distinctUntilChanged
|> map { value -> AccountRecordId? in
if let value = value, value.running {
return accountId
} else {
return nil
}
}
|> distinctUntilChanged
} else {
return .single(nil)
}
}*/
let wakeupManager = SharedWakeupManager(beginBackgroundTask: { name, expiration in
let id = application.beginBackgroundTask(withName: name, expirationHandler: expiration)
Logger.shared.log("App \(self.episodeId)", "Begin background task \(name): \(id)")
print("App \(self.episodeId)", "Begin background task \(name): \(id)")
return id
}, endBackgroundTask: { id in
print("App \(self.episodeId)", "End background task \(id)")
Logger.shared.log("App \(self.episodeId)", "End background task \(id)")
application.endBackgroundTask(id)
}, backgroundTimeRemaining: { application.backgroundTimeRemaining }, acquireIdleExtension: {
return applicationBindings.pushIdleTimerExtension()
}, activeAccounts: sharedContext.activeAccountContexts |> map { ($0.0?.account, $0.1.map { ($0.0, $0.1.account) }) }, liveLocationPolling: liveLocationPolling, watchTasks: .single(nil), inForeground: applicationBindings.applicationInForeground, hasActiveAudioSession: self.hasActiveAudioSession.get(), notificationManager: notificationManager, mediaManager: sharedContext.mediaManager, callManager: sharedContext.callManager, accountUserInterfaceInUse: { id in
return sharedContext.accountUserInterfaceInUse(id)
})
let sharedApplicationContext = SharedApplicationContext(sharedContext: sharedContext, notificationManager: notificationManager, wakeupManager: wakeupManager)
sharedApplicationContext.sharedContext.mediaManager.overlayMediaManager.attachOverlayMediaController(sharedApplicationContext.overlayMediaController)
return accountManager.transaction { transaction -> (SharedApplicationContext, LoggingSettings) in
return (sharedApplicationContext, transaction.getSharedData(SharedDataKeys.loggingSettings)?.get(LoggingSettings.self) ?? LoggingSettings.defaultSettings)
}
}
self.sharedContextPromise.set(sharedContextSignal
|> mapToSignal { sharedApplicationContext, loggingSettings -> Signal<SharedApplicationContext, NoError> in
Logger.shared.logToFile = loggingSettings.logToFile
Logger.shared.logToConsole = loggingSettings.logToConsole
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
return .single(sharedApplicationContext)
})
//let watchManagerArgumentsPromise = Promise<WatchManagerArguments?>()
self.context.set(self.sharedContextPromise.get()
|> deliverOnMainQueue
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext?, NoError> in
return sharedApplicationContext.sharedContext.activeAccountContexts
|> map { primary, _, _ -> AccountContext? in
return primary
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs !== rhs {
return false
}
return true
})
|> mapToSignal { context -> Signal<(AccountContext, CallListSettings)?, NoError> in
return sharedApplicationContext.sharedContext.accountManager.transaction { transaction -> CallListSettings? in
return transaction.getSharedData(ApplicationSpecificSharedDataKeys.callListSettings)?.get(CallListSettings.self)
}
|> reduceLeft(value: nil) { current, updated -> CallListSettings? in
var result: CallListSettings?
if let updated = updated {
result = updated
} else if let current = current {
result = current
}
return result
}
|> map { callListSettings -> (AccountContext, CallListSettings)? in
if let context = context {
return (context, callListSettings ?? .defaultSettings)
} else {
return nil
}
}
}
|> deliverOnMainQueue
|> map { accountAndSettings -> AuthorizedApplicationContext? in
return accountAndSettings.flatMap { context, callListSettings in
return AuthorizedApplicationContext(sharedApplicationContext: sharedApplicationContext, mainWindow: self.mainWindow, watchManagerArguments: .single(nil), context: context as! AccountContextImpl, accountManager: sharedApplicationContext.sharedContext.accountManager, showCallsTab: callListSettings.showTab, reinitializedNotificationSettings: {
let _ = (self.context.get()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
if let context = context {
self.registerForNotifications(context: context.context, authorize: false)
}
})
})
}
}
})
self.authContext.set(self.sharedContextPromise.get()
|> deliverOnMainQueue
|> mapToSignal { sharedApplicationContext -> Signal<UnauthorizedApplicationContext?, NoError> in
return sharedApplicationContext.sharedContext.activeAccountContexts
|> map { primary, accounts, auth -> (AccountContext?, UnauthorizedAccount, [AccountContext])? in
if let auth = auth {
return (primary, auth, Array(accounts.map({ $0.1 })))
} else {
return nil
}
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs?.1 !== rhs?.1 {
return false
}
return true
})
|> mapToSignal { authAndAccounts -> Signal<(UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))?, NoError> in
if let (primary, auth, accounts) = authAndAccounts {
let phoneNumbers = combineLatest(accounts.map { context -> Signal<(AccountRecordId, String, Bool)?, NoError> in
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> (AccountRecordId, String, Bool)? in
if case let .user(user) = peer, let phone = user.phone {
return (context.account.id, phone, context.account.testingEnvironment)
} else {
return nil
}
}
})
return phoneNumbers
|> map { phoneNumbers -> (UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))? in
var primaryNumber: (String, AccountRecordId, Bool)?
if let primary = primary {
for idAndNumber in phoneNumbers {
if let (id, number, testingEnvironment) = idAndNumber, id == primary.account.id {
primaryNumber = (number, id, testingEnvironment)
break
}
}
}
return (auth, (primaryNumber, phoneNumbers.compactMap({ $0.flatMap({ ($0.1, $0.0, $0.2) }) })))
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue
|> map { accountAndSettings -> UnauthorizedApplicationContext? in
return accountAndSettings.flatMap { account, otherAccountPhoneNumbers in
return UnauthorizedApplicationContext(apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, sharedContext: sharedApplicationContext.sharedContext, account: account, otherAccountPhoneNumbers: otherAccountPhoneNumbers)
}
}
})
let contextReadyDisposable = MetaDisposable()
let startTime = CFAbsoluteTimeGetCurrent()
self.contextDisposable.set((self.context.get()
|> deliverOnMainQueue).start(next: { context in
print("Application: context took \(CFAbsoluteTimeGetCurrent() - startTime) to become available")
var network: Network?
if let context = context {
network = context.context.account.network
}
Logger.shared.log("App \(self.episodeId)", "received context \(String(describing: context)) account \(String(describing: context?.context.account.id)) network \(String(describing: network))")
let firstTime = self.contextValue == nil
if let contextValue = self.contextValue {
contextValue.passcodeController?.dismiss()
contextValue.context.account.shouldExplicitelyKeepWorkerConnections.set(.single(false))
contextValue.context.account.shouldKeepBackgroundDownloadConnections.set(.single(false))
}
self.contextValue = context
if let context = context {
setupLegacyComponents(context: context.context)
let isReady = context.isReady.get()
contextReadyDisposable.set((isReady
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
let readyTime = CFAbsoluteTimeGetCurrent() - startTime
if readyTime > 0.5 {
print("Application: context took \(readyTime) to become ready")
}
print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms")
self.mainWindow.debugAction = nil
self.mainWindow.viewController = context.rootController
if firstTime {
let layer = context.rootController.view.layer
layer.allowsGroupOpacity = true
layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in
if let layer = layer {
layer.allowsGroupOpacity = false
}
})
}
self.mainWindow.forEachViewController({ controller in
if let controller = controller as? TabBarAccountSwitchController {
if let rootController = self.mainWindow.viewController as? TelegramRootController {
if let tabsController = rootController.viewControllers.first as? TabBarController {
for i in 0 ..< tabsController.controllers.count {
if let _ = tabsController.controllers[i] as? (SettingsController & ViewController) {
let sourceNodes = tabsController.sourceNodesForController(at: i)
if let sourceNodes = sourceNodes {
controller.dismiss(sourceNodes: sourceNodes)
}
return false
}
}
}
}
}
return true
})
self.mainWindow.topLevelOverlayControllers = [context.sharedApplicationContext.overlayMediaController, context.notificationController]
(context.context.sharedContext as? SharedAccountContextImpl)?.notificationController = context.notificationController
var authorizeNotifications = true
if #available(iOS 10.0, *) {
authorizeNotifications = false
}
self.registerForNotifications(context: context.context, authorize: authorizeNotifications)
self.resetIntentsIfNeeded(context: context.context)
}))
} else {
self.mainWindow.viewController = nil
self.mainWindow.topLevelOverlayControllers = []
contextReadyDisposable.set(nil)
}
}))
let authContextReadyDisposable = MetaDisposable()
self.authContextDisposable.set((self.authContext.get()
|> deliverOnMainQueue).start(next: { context in
var network: Network?
if let context = context {
network = context.account.network
}
Logger.shared.log("App \(self.episodeId)", "received auth context \(String(describing: context)) account \(String(describing: context?.account.id)) network \(String(describing: network))")
if let authContextValue = self.authContextValue {
authContextValue.account.shouldBeServiceTaskMaster.set(.single(.never))
if authContextValue.authorizationCompleted {
let accountId = authContextValue.account.id
let _ = (self.context.get()
|> filter { context in
return context?.context.account.id == accountId
}
|> take(1)
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|> deliverOnMainQueue).start(completed: {
Queue.mainQueue().after(0.75) {
authContextValue.rootController.view.endEditing(true)
authContextValue.rootController.dismiss()
}
})
} else {
authContextValue.rootController.view.endEditing(true)
authContextValue.rootController.dismiss()
}
}
self.authContextValue = context
if let context = context {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.mainWindow.present(statusController, on: .root)
return ActionDisposable { [weak statusController] in
Queue.mainQueue().async() {
statusController?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.5, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let isReady: Signal<Bool, NoError> = context.isReady.get()
authContextReadyDisposable.set((isReady
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
progressDisposable.dispose()
self.mainWindow.present(context.rootController, on: .root)
}))
} else {
authContextReadyDisposable.set(nil)
}
}))
let logoutDataSignal: Signal<(AccountManager, Set<PeerId>), NoError> = self.sharedContextPromise.get()
|> take(1)
|> mapToSignal { sharedContext -> Signal<(AccountManager<TelegramAccountManagerTypes>, Set<PeerId>), NoError> in
return sharedContext.sharedContext.activeAccountContexts
|> map { _, accounts, _ -> Set<PeerId> in
return Set(accounts.map { $0.1.account.peerId })
}
|> reduceLeft(value: Set<PeerId>()) { current, updated, emit in
if !current.isEmpty {
emit(current.subtracting(current.intersection(updated)))
}
return updated
}
|> map { loggedOutAccountPeerIds -> (AccountManager<TelegramAccountManagerTypes>, Set<PeerId>) in
return (sharedContext.sharedContext.accountManager, loggedOutAccountPeerIds)
}
}
self.logoutDisposable.set(logoutDataSignal.start(next: { accountManager, loggedOutAccountPeerIds in
let _ = (updateIntentsSettingsInteractively(accountManager: accountManager) { current in
var updated = current
for peerId in loggedOutAccountPeerIds {
deleteAllStoryDrafts(peerId: peerId)
if peerId == updated.account {
deleteAllSendMessageIntents()
updated = updated.withUpdatedAccount(nil)
break
}
}
return updated
}).start()
}))
/*self.watchCommunicationManagerPromise.set(watchCommunicationManager(context: self.context.get() |> flatMap { WatchCommunicationManagerContext(context: $0.context) }, allowBackgroundTimeExtension: { timeout in
let _ = (self.sharedContextPromise.get()
|> take(1)).start(next: { sharedContext in
sharedContext.wakeupManager.allowBackgroundTimeExtension(timeout: timeout)
})
}))
let _ = self.watchCommunicationManagerPromise.get().start(next: { manager in
if let manager = manager {
watchManagerArgumentsPromise.set(.single(manager.arguments))
} else {
watchManagerArgumentsPromise.set(.single(nil))
}
})*/
self.resetBadge()
if #available(iOS 9.1, *) {
self.quickActionsDisposable.set((self.context.get()
|> mapToSignal { context -> Signal<[ApplicationShortcutItem], NoError> in
if let context = context {
let presentationData = context.context.sharedContext.currentPresentationData.with { $0 }
return activeAccountsAndPeers(context: context.context)
|> take(1)
|> map { primaryAndAccounts -> (AccountContext, EnginePeer, Int32)? in
return primaryAndAccounts.1.first
}
|> map { accountAndPeer -> String? in
if let (_, peer, _) = accountAndPeer {
return peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} else {
return nil
}
} |> mapToSignal { otherAccountName -> Signal<[ApplicationShortcutItem], NoError> in
let presentationData = context.context.sharedContext.currentPresentationData.with { $0 }
return .single(applicationShortcutItems(strings: presentationData.strings, otherAccountName: otherAccountName))
}
} else {
return .single([])
}
}
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { items in
if items.isEmpty {
UIApplication.shared.shortcutItems = nil
} else {
UIApplication.shared.shortcutItems = items.map({ $0.shortcutItem() })
}
}))
}
let _ = self.isInForegroundPromise.get().start(next: { value in
Logger.shared.log("App \(self.episodeId)", "isInForeground = \(value)")
})
let _ = self.isActivePromise.get().start(next: { value in
Logger.shared.log("App \(self.episodeId)", "isActive = \(value)")
})
if let url = launchOptions?[.url] {
if let url = url as? URL, url.scheme == "tg" || url.scheme == buildConfig.appSpecificUrlScheme {
self.openUrlWhenReady(url: url)
} else if let urlString = url as? String, urlString.lowercased().hasPrefix("tg:") || urlString.lowercased().hasPrefix("\(buildConfig.appSpecificUrlScheme):"), let url = URL(string: urlString) {
self.openUrlWhenReady(url: url)
}
}
if application.applicationState == .active {
self.isInForegroundValue = true
self.isInForegroundPromise.set(true)
self.isActiveValue = true
self.isActivePromise.set(true)
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
self.runForegroundTasks()
}
DeviceProximityManager.shared().proximityChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.mainWindow.setProximityDimHidden(!value)
}
}
if UIApplication.shared.isStatusBarHidden {
UIApplication.shared.internalSetStatusBarHidden(false, animation: .none)
}
/*if #available(iOS 13.0, *) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: baseAppBundleId + ".refresh", using: nil, launchHandler: { task in
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
sharedApplicationContext.wakeupManager.replaceCurrentExtensionWithExternalTime(completion: {
task.setTaskCompleted(success: true)
}, timeout: 29.0)
let _ = (self.context.get()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
guard let context = context else {
return
}
sharedApplicationContext.notificationManager.beginPollingState(account: context.context.account)
})
})
})
}*/
self.maybeCheckForUpdates()
#if canImport(AppCenter)
if !buildConfig.isAppStoreBuild, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty {
AppCenter.start(withAppSecret: buildConfig.appCenterId, services: [
Crashes.self
])
}
#endif
if #available(iOS 13.0, *) {
let taskId = "\(baseAppBundleId).cleanup"
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: DispatchQueue.main) { task in
Logger.shared.log("App \(self.episodeId)", "Executing cleanup task")
let disposable = self.runCacheReindexTasks(lowImpact: true, completion: {
Logger.shared.log("App \(self.episodeId)", "Completed cleanup task")
task.setTaskCompleted(success: true)
})
task.expirationHandler = {
disposable.dispose()
task.setTaskCompleted(success: false)
}
}
BGTaskScheduler.shared.getPendingTaskRequests(completionHandler: { tasks in
if tasks.contains(where: { $0.identifier == taskId }) {
Logger.shared.log("App \(self.episodeId)", "Already have a cleanup task pending")
return
}
let request = BGProcessingTaskRequest(identifier: taskId)
request.requiresExternalPower = true
request.requiresNetworkConnectivity = false
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
Logger.shared.log("App \(self.episodeId)", "Error submitting background task request: \(e)")
}
})
}
let timestamp = Int(CFAbsoluteTimeGetCurrent())
let minReindexTimestamp = timestamp - 2 * 24 * 60 * 60
if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp_v2") as? NSNumber, indexTimestamp.intValue >= minReindexTimestamp {
} else {
UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp_v2")
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground")
let _ = self.runCacheReindexTasks(lowImpact: true, completion: {
Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done")
})
}
if #available(iOS 12.0, *) {
UIApplication.shared.registerForRemoteNotifications()
}
let _ = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
var previousReportedMemoryConsumption = 0
let _ = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
let value = getMemoryConsumption()
if abs(value - previousReportedMemoryConsumption) > 1 * 1024 * 1024 {
previousReportedMemoryConsumption = value
Logger.shared.log("App \(self.episodeId)", "Memory consumption: \(value / (1024 * 1024)) MB")
if self.contextValue?.context.sharedContext.immediateExperimentalUISettings.crashOnMemoryPressure == true {
let memoryUsageOverlayView: UILabel
if let current = self.memoryUsageOverlayView {
memoryUsageOverlayView = current
} else {
memoryUsageOverlayView = UILabel()
if #available(iOS 13.0, *) {
memoryUsageOverlayView.textColor = .label
} else {
memoryUsageOverlayView.textColor = .black
}
memoryUsageOverlayView.font = Font.regular(11.0)
memoryUsageOverlayView.layer.zPosition = 1000.0
self.memoryUsageOverlayView = memoryUsageOverlayView
self.window?.addSubview(memoryUsageOverlayView)
memoryUsageOverlayView.center = CGPoint(x: 5.0, y: 36.0)
}
memoryUsageOverlayView.text = "\(value / (1024 * 1024)) MB"
memoryUsageOverlayView.sizeToFit()
} else {
if let memoryUsageOverlayView = self.memoryUsageOverlayView {
self.memoryUsageOverlayView = nil
memoryUsageOverlayView.removeFromSuperview()
}
}
if !buildConfig.isAppStoreBuild {
if value >= 2000 * 1024 * 1024 {
if self.contextValue?.context.sharedContext.immediateExperimentalUISettings.crashOnMemoryPressure == true {
}
}
}
}
})
//self.addBackgroundDownloadTask()
let reflectorBenchmarkDisposable = MetaDisposable()
let runReflectorBenchmarkDisposable = MetaDisposable()
let _ = (self.context.get()
|> deliverOnMainQueue).startStandalone(next: { context in
reflectorBenchmarkDisposable.set(nil)
runReflectorBenchmarkDisposable.set(nil)
guard let context = context?.context else {
return
}
var defaultAutoBenchmarkReflectors = false
if case .internal = context.sharedContext.applicationBindings.appBuildType {
defaultAutoBenchmarkReflectors = true
}
if context.sharedContext.immediateExperimentalUISettings.autoBenchmarkReflectors ?? defaultAutoBenchmarkReflectors {
reflectorBenchmarkDisposable.set((context.sharedContext.applicationBindings.applicationInForeground
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { value in
if value {
let signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
reflectorBenchmark?.start(completion: { results in
subscriber.putNext(results)
subscriber.putCompletion()
})
return ActionDisposable {
reflectorBenchmark = nil
}
}
|> runOn(.mainQueue())
|> delay(Double.random(in: 1.0 ..< 5.0), queue: Queue.mainQueue())
runReflectorBenchmarkDisposable.set(signal.startStrict(next: { results in
print("Reflector banchmark:\nBandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms")
}))
} else {
runReflectorBenchmarkDisposable.set(nil)
}
}))
}
})
return true
}
private var backgroundSessionSourceDataDisposables: [String: Disposable] = [:]
private var backgroundUploadResultSubscribers: [String: Bag<(String?) -> Void>] = [:]
func uploadInBackround(postbox: Postbox, resource: MediaResource) -> Signal<String?, NoError> {
let baseAppBundleId = Bundle.main.bundleIdentifier!
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
let signal = Signal<Never, NoError> { subscriber in
let disposable = MetaDisposable()
let _ = session.getAllTasks(completionHandler: { tasks in
var alreadyExists = false
for task in tasks {
if let originalRequest = task.originalRequest {
if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") {
if resource.id.stringRepresentation == requestResourceId {
alreadyExists = true
break
}
}
}
}
if !alreadyExists, self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] == nil {
self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] = (Signal<Never, NoError> { subscriber in
let dataDisposable = (postbox.mediaBox.resourceData(resource)
|> deliverOnMainQueue).start(next: { data in
if data.complete {
self.addBackgroundUploadTask(id: resource.id.stringRepresentation, path: data.path)
}
})
let fetchDisposable = postbox.mediaBox.fetchedResource(resource, parameters: nil).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
}).start()
}
})
return disposable
}
|> runOn(.mainQueue())
return Signal { subscriber in
let bag: Bag<(String?) -> Void>
if let current = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] {
bag = current
} else {
bag = Bag()
self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] = bag
}
let index = bag.add { result in
subscriber.putNext(result)
subscriber.putCompletion()
}
let workDisposable = signal.start()
return ActionDisposable {
workDisposable.dispose()
Queue.mainQueue().async {
if let bag = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] {
bag.remove(index)
if bag.isEmpty {
//TODO:cancel tasks
}
}
}
}
}
|> runOn(.mainQueue())
}
private func addBackgroundUploadTask(id: String, path: String) {
let baseAppBundleId = Bundle.main.bundleIdentifier!
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
let fileName = "upload-\(UInt32.random(in: 0 ... UInt32.max))"
let uploadFilePath = NSTemporaryDirectory() + "/" + fileName
guard let sourceFile = ManagedFile(queue: nil, path: uploadFilePath, mode: .readwrite) else {
return
}
guard let inFile = ManagedFile(queue: nil, path: path, mode: .read) else {
return
}
let boundary = UUID().uuidString
var headerData = Data()
headerData.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
headerData.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
headerData.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
var footerData = Data()
footerData.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
let _ = sourceFile.write(headerData)
let bufferSize = 512 * 1024
let buffer = malloc(bufferSize)!
defer {
free(buffer)
}
while true {
let readBytes = inFile.read(buffer, bufferSize)
if readBytes <= 0 {
break
} else {
let _ = sourceFile.write(buffer, count: readBytes)
}
}
let _ = sourceFile.write(footerData)
sourceFile._unsafeClose()
inFile._unsafeClose()
var request = URLRequest(url: URL(string: "http://localhost:25478/upload?token=f9403fc5f537b4ab332d")!)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue(id, forHTTPHeaderField: "tresource")
let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: uploadFilePath))
task.resume()
}
private func addBackgroundDownloadTask() {
let baseAppBundleId = Bundle.main.bundleIdentifier!
let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession")
var request = URLRequest(url: URL(string: "https://example.com/\(UInt64.random(in: 0 ... UInt64.max))")!)
request.httpMethod = "GET"
let task = session.downloadTask(with: request)
Logger.shared.log("App \(self.episodeId)", "adding download task \(String(describing: request.url))")
task.earliestBeginDate = Date(timeIntervalSinceNow: 30.0)
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
Logger.shared.log("App \(self.episodeId)", "completed download task \(String(describing: task.originalRequest?.url)) error: \(String(describing: error))")
if let response = task.response as? HTTPURLResponse {
if let originalRequest = task.originalRequest {
if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") {
if let bag = self.backgroundUploadResultSubscribers[requestResourceId] {
for item in bag.copyItems() {
item("http server: \(response.allHeaderFields)")
}
}
}
}
}
}
private func runCacheReindexTasks(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
let disposable = MetaDisposable()
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
var signals: Signal<Never, NoError> = .complete()
for (_, context, _) in activeAccounts.accounts {
signals = signals |> then(context.account.cleanupTasks(lowImpact: lowImpact))
}
disposable.set(signals.start(completed: {
completion()
}))
})
})
return disposable
}
private func resetBadge() {
var resetOnce = true
self.badgeDisposable.set((self.context.get()
|> mapToSignal { context -> Signal<Int32, NoError> in
if let context = context {
return context.applicationBadge
} else {
return .single(0)
}
}
|> deliverOnMainQueue).start(next: { count in
if resetOnce {
resetOnce = false
if count == 0 {
//UIApplication.shared.applicationIconBadgeNumber = 1
}
}
UIApplication.shared.applicationIconBadgeNumber = Int(count)
}))
}
func applicationWillResignActive(_ application: UIApplication) {
self.isActiveValue = false
self.isActivePromise.set(false)
self.clearNotificationsManager?.commitNow()
if let navigationController = self.mainWindow.viewController as? NavigationController {
for controller in navigationController.viewControllers {
if let controller = controller as? TabBarController {
for subController in controller.controllers {
subController.forEachController { controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
}
}
}
}
}
self.mainWindow.forEachViewController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
func applicationDidEnterBackground(_ application: UIApplication) {
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
var extendNow = false
if #available(iOS 9.0, *) {
if !ProcessInfo.processInfo.isLowPowerModeEnabled {
extendNow = true
}
}
if !sharedApplicationContext.sharedContext.energyUsageSettings.extendBackgroundWork {
extendNow = false
}
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0, extendNow: extendNow)
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
for (_, context, _) in activeAccounts.accounts {
context.account.postbox.clearCaches()
}
})
})
self.isInForegroundValue = false
self.isInForegroundPromise.set(false)
self.isActiveValue = false
self.isActivePromise.set(false)
final class TaskIdHolder {
var taskId: UIBackgroundTaskIdentifier?
}
let taskIdHolder = TaskIdHolder()
taskIdHolder.taskId = application.beginBackgroundTask(withName: "lock", expirationHandler: {
if let taskId = taskIdHolder.taskId {
UIApplication.shared.endBackgroundTask(taskId)
}
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: {
if let taskId = taskIdHolder.taskId {
UIApplication.shared.endBackgroundTask(taskId)
}
})
}
func applicationWillEnterForeground(_ application: UIApplication) {
if self.isActiveValue {
self.isInForegroundValue = true
self.isInForegroundPromise.set(true)
} else {
if #available(iOSApplicationExtension 12.0, *) {
DispatchQueue.main.async {
self.isInForegroundValue = true
self.isInForegroundPromise.set(true)
}
}
}
self.runForegroundTasks()
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
}
func runForegroundTasks() {
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
for (_, context, _) in activeAccounts.accounts {
(context.downloadedMediaStoreManager as? DownloadedMediaStoreManagerImpl)?.runTasks()
}
})
})
}
func applicationDidBecomeActive(_ application: UIApplication) {
self.isInForegroundValue = true
self.isInForegroundPromise.set(true)
self.isActiveValue = true
self.isActivePromise.set(true)
self.resetBadge()
self.maybeCheckForUpdates()
SharedDisplayLinkDriver.shared.updateForegroundState(self.isActiveValue)
func cancelWindowPanGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if let recognizer = recognizer as? WindowPanRecognizer {
recognizer.cancel()
}
}
}
for subview in view.subviews {
cancelWindowPanGestures(view: subview)
}
}
//cancelWindowPanGestures(view: self.mainWindow.hostView.containerView)
}
func applicationWillTerminate(_ application: UIApplication) {
Logger.shared.log("App \(self.episodeId)", "terminating")
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Logger.shared.log("App \(self.episodeId)", "register for notifications: didRegisterForRemoteNotificationsWithDeviceToken (deviceToken: \(hexString(deviceToken)))")
self.notificationTokenPromise.set(.single(deviceToken))
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
Logger.shared.log("App \(self.episodeId)", "register for notifications: didFailToRegisterForRemoteNotificationsWithError (error: \(error))")
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0)
})
var redactedPayload = userInfo
if var aps = redactedPayload["aps"] as? [AnyHashable: Any] {
if Logger.shared.redactSensitiveData {
if aps["alert"] != nil {
aps["alert"] = "[[redacted]]"
}
if aps["body"] != nil {
aps["body"] = "[[redacted]]"
}
}
redactedPayload["aps"] = aps
}
Logger.shared.log("App \(self.episodeId)", "remoteNotification: \(redactedPayload)")
if let firebaseAuth = redactedPayload["com.google.firebase.auth"] as? String {
guard let firebaseAuthData = firebaseAuth.data(using: .utf8), let firebaseJson = try? JSONSerialization.jsonObject(with: firebaseAuthData) else {
completionHandler(.newData)
return
}
guard let firebaseDict = firebaseJson as? [String: Any] else {
completionHandler(.newData)
return
}
if let receipt = firebaseDict["receipt"] as? String, let secret = firebaseDict["secret"] as? String {
var firebaseSecrets = self.firebaseSecrets
firebaseSecrets[receipt] = secret
self.firebaseSecrets = firebaseSecrets
}
completionHandler(.newData)
return
}
if let nonce = redactedPayload["verify_nonce"] as? String, let secret = redactedPayload["verify_secret"] as? String {
var firebaseRequestVerificationSecrets = self.firebaseRequestVerificationSecrets
firebaseRequestVerificationSecrets[nonce] = secret
self.firebaseRequestVerificationSecrets = firebaseRequestVerificationSecrets
completionHandler(.newData)
return
}
if userInfo["p"] == nil {
completionHandler(.noData)
return
}
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
sharedApplicationContext.wakeupManager.replaceCurrentExtensionWithExternalTime(completion: {
completionHandler(.newData)
}, timeout: 29.0)
sharedApplicationContext.notificationManager.addNotification(userInfo)
})
}
public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
if #available(iOS 9.0, *) {
if case PKPushType.voIP = type {
Logger.shared.log("App \(self.episodeId)", "pushRegistry credentials: \(credentials.token as NSData)")
self.voipTokenPromise.set(.single(credentials.token))
}
}
}
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry didReceiveIncomingPushWith \(payload.dictionaryPayload)")
self.pushRegistryImpl(registry, didReceiveIncomingPushWith: payload, for: type, completion: completion)
}
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry didReceiveIncomingPushWith \(payload.dictionaryPayload)")
self.pushRegistryImpl(registry, didReceiveIncomingPushWith: payload, for: type, completion: {})
}
private func pushRegistryImpl(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry processing push notification")
let decryptedPayloadAndAccountId: ([AnyHashable: Any], AccountRecordId)?
if let accountIdString = payload.dictionaryPayload["accountId"] as? String, let accountId = Int64(accountIdString) {
decryptedPayloadAndAccountId = (payload.dictionaryPayload, AccountRecordId(rawValue: accountId))
} else {
guard var encryptedPayload = payload.dictionaryPayload["p"] as? String else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "encryptedPayload is nil")
completion()
return
}
encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+")
encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/")
while encryptedPayload.count % 4 != 0 {
encryptedPayload.append("=")
}
guard let payloadData = Data(base64Encoded: encryptedPayload) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode encryptedPayload")
completion()
return
}
guard let keyId = notificationPayloadKeyId(data: payloadData) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't parse payload key id")
completion()
return
}
guard let accountManagerState = self.accountManagerState else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "accountManagerState is nil")
completion()
return
}
var maybeAccountId: AccountRecordId?
var maybeNotificationKey: MasterNotificationKey?
for key in accountManagerState.notificationKeys {
if key.id == keyId {
maybeAccountId = key.accountId
maybeNotificationKey = MasterNotificationKey(id: key.id, data: key.key)
break
}
}
guard let accountId = maybeAccountId, let notificationKey = maybeNotificationKey else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "accountId or notificationKey is nil")
completion()
return
}
guard let decryptedPayload = decryptedNotificationPayload(key: notificationKey, data: payloadData) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decrypt payload")
completion()
return
}
guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [AnyHashable: Any] else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode payload json")
completion()
return
}
decryptedPayloadAndAccountId = (payloadJson, accountId)
}
guard let (payloadJson, accountId) = decryptedPayloadAndAccountId else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "decryptedPayloadAndAccountId is nil")
completion()
return
}
let phoneNumber = payloadJson["phoneNumber"] as? String
if let fromIdString = payloadJson["from_id"] as? String, let fromId = Int64(fromIdString), let groupCallIdString = payloadJson["group_call_id"] as? String, let groupCallId = Int64(groupCallIdString), let messageIdString = payloadJson["msg_id"] as? String, let messageId = Int32(messageIdString), let fromTitle = payloadJson["from_title"] as? String {
guard let callKitIntegration = CallKitIntegration.shared else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "CallKitIntegration is not available")
completion()
return
}
var isVideo = false
if let isVideoString = payloadJson["video"] as? String, let isVideoValue = Int32(isVideoString) {
isVideo = isVideoValue != 0
} else if let isVideoString = payloadJson["video"] as? String, let isVideoValue = Bool(isVideoString) {
isVideo = isVideoValue
}
let fromPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(fromId))
let messageId = MessageId(peerId: fromPeerId, namespace: Namespaces.Message.Cloud, id: messageId)
let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id)
var strings: PresentationStrings = defaultPresentationStrings
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
strings = sharedApplicationContext.sharedContext.currentPresentationData.with { $0.strings }
})
let displayTitle: String
if let memberCountString = payloadJson["member_count"] as? String, let memberCount = Int(memberCountString) {
displayTitle = strings.Call_IncomingGroupCallTitle_Multiple(Int32(memberCount)).replacingOccurrences(of: "{}", with: fromTitle)
} else {
displayTitle = strings.Call_IncomingGroupCallTitle_Single(fromTitle).string
}
callKitIntegration.reportIncomingCall(
uuid: internalId,
stableId: groupCallId,
handle: "\(fromPeerId.id._internalGetInt64Value())",
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
isVideo: isVideo,
displayTitle: displayTitle,
completion: { error in
if let error = error {
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode")
} else {
Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)")
/*Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil))
}
}*/
}
}
}
)
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
var processed = false
for (_, context, _) in activeAccounts.accounts {
if context.account.id == accountId {
context.account.callSessionManager.addConferenceInvitationMessages(ids: [(messageId, IncomingConferenceTermporaryExternalInfo(callId: groupCallId, isVideo: isVideo))])
let disposable = MetaDisposable()
self.watchedCallsDisposables.add(disposable)
if let callManager = context.sharedContext.callManager {
let signal = combineLatest(queue: .mainQueue(), context.account.callSessionManager.ringingStates()
|> map { ringingStates -> Bool in
for state in ringingStates {
if state.id == internalId {
return true
}
}
return false
},
callManager.currentGroupCallSignal
|> map { currentGroupCall -> Bool in
if case let .group(groupCall) = currentGroupCall {
if groupCall.internalId == internalId {
return true
}
}
return false
}
)
|> mapToSignal { exists0, exists1 -> Signal<Void, NoError> in
if !(exists0 || exists1) {
return .single(Void())
|> delay(1.0, queue: .mainQueue())
}
return .never()
}
disposable.set((signal
|> take(1)
|> deliverOnMainQueue).startStrict(next: { _ in
callKitIntegration.dropCall(uuid: internalId)
}))
}
processed = true
break
}
}
if !processed {
callKitIntegration.dropCall(uuid: internalId)
}
})
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0)
if case PKPushType.voIP = type {
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)")
sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload)
}
})
} else {
guard var updateString = payloadJson["updates"] as? String else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "updates is nil")
self.reportFailedIncomingCallKitCall()
completion()
return
}
updateString = updateString.replacingOccurrences(of: "-", with: "+")
updateString = updateString.replacingOccurrences(of: "_", with: "/")
while updateString.count % 4 != 0 {
updateString.append("=")
}
guard let updateData = Data(base64Encoded: updateString) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode updateData")
self.reportFailedIncomingCallKitCall()
completion()
return
}
guard let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't extract call update")
self.reportFailedIncomingCallKitCall()
completion()
return
}
guard let callKitIntegration = CallKitIntegration.shared else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "CallKitIntegration is not available")
completion()
return
}
callKitIntegration.reportIncomingCall(
uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId),
stableId: callUpdate.callId,
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
isVideo: callUpdate.isVideo,
displayTitle: callUpdate.peer.debugDisplayTitle,
completion: { error in
if let error = error {
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode")
} else {
Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)")
/*Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil))
}
}*/
}
}
}
)
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
var processed = false
for (_, context, _) in activeAccounts.accounts {
if context.account.id == accountId {
context.account.stateManager.processIncomingCallUpdate(data: updateData, completion: { _ in
})
let disposable = MetaDisposable()
self.watchedCallsDisposables.add(disposable)
disposable.set((context.account.callSessionManager.callState(internalId: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
|> deliverOnMainQueue).start(next: { state in
switch state.state {
case .terminated:
callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
default:
break
}
}))
processed = true
break
}
}
if !processed {
callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId))
}
})
sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0)
if case PKPushType.voIP = type {
Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)")
sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload)
}
})
}
Logger.shared.log("App \(self.episodeId) PushRegistry", "Invoking completion handler")
completion()
}
public func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
Logger.shared.log("App \(self.episodeId)", "invalidated token for \(type)")
}
private func reportFailedIncomingCallKitCall() {
if #available(iOS 14.4, *) {
guard let callKitIntegration = CallKitIntegration.shared else {
return
}
let uuid = CallSessionInternalId()
callKitIntegration.reportIncomingCall(
uuid: uuid,
stableId: Int64.random(in: Int64.min ... Int64.max),
handle: "Unknown",
phoneNumber: nil,
isVideo: false,
displayTitle: "Unknown",
completion: { error in
if let error = error {
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
Logger.shared.log("PresentationCall", "reportFailedIncomingCallKitCall device in DND mode")
} else {
Logger.shared.log("PresentationCall", "reportFailedIncomingCallKitCall error \(error)")
}
}
}
)
Queue.mainQueue().after(1.0, {
callKitIntegration.dropCall(uuid: uuid)
})
}
}
private func authorizedContext() -> Signal<AuthorizedApplicationContext, NoError> {
return self.context.get()
|> mapToSignal { context -> Signal<AuthorizedApplicationContext, NoError> in
if let context = context {
return .single(context)
} else {
return .complete()
}
}
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool {
self.openUrl(url: url)
return true
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
self.openUrl(url: url)
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
guard self.openUrlInProgress != url else {
return true
}
self.openUrl(url: url)
return true
}
func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
self.openUrl(url: url)
return true
}
private func openUrl(url: URL) {
let _ = (self.sharedContextPromise.get()
|> take(1)
|> mapToSignal { sharedApplicationContext -> Signal<(SharedAccountContextImpl, AuthorizedApplicationContext?, UnauthorizedApplicationContext?), NoError> in
combineLatest(self.context.get(), self.authContext.get())
|> filter { $0 != nil || $1 != nil }
|> take(1)
|> map { context, authContext -> (SharedAccountContextImpl, AuthorizedApplicationContext?, UnauthorizedApplicationContext?) in
return (sharedApplicationContext.sharedContext, context, authContext)
}
}
|> deliverOnMainQueue).start(next: { sharedContext, context, authContext in
if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(sharedContext: sharedContext, url: url) {
authContext.rootController.applyConfirmationCode(confirmationCode)
} else if let context = context {
context.openUrl(url)
} else if let authContext = authContext {
if let proxyData = parseProxyUrl(sharedContext: sharedContext, url: url) {
authContext.rootController.view.endEditing(true)
let presentationData = authContext.sharedContext.currentPresentationData.with { $0 }
let controller = ProxyServerActionSheetController(presentationData: presentationData, accountManager: authContext.sharedContext.accountManager, postbox: authContext.account.postbox, network: authContext.account.network, server: proxyData, updatedPresentationData: nil)
authContext.rootController.currentWindow?.present(controller, on: PresentationSurfaceLevel.root, blockInteraction: false, completion: {})
} else if let secureIdData = parseSecureIdUrl(url) {
let presentationData = authContext.sharedContext.currentPresentationData.with { $0 }
authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Passport_NotLoggedInMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Calls_NotNow, action: {
if let callbackUrl = URL(string: secureIdCallbackUrl(with: secureIdData.callbackUrl, peerId: secureIdData.peerId, result: .cancel, parameters: [:])) {
UIApplication.shared.open(callbackUrl, options: [:], completionHandler: nil)
}
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {})
}
}
})
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if #available(iOS 10.0, *) {
var startCallContacts: [INPerson]?
var isVideo = false
if let startCallIntent = userActivity.interaction?.intent as? SupportedStartCallIntent {
startCallContacts = startCallIntent.contacts
isVideo = false
} else if let startCallIntent = userActivity.interaction?.intent as? SupportedStartVideoCallIntent {
startCallContacts = startCallIntent.contacts
isVideo = true
}
if let startCallContacts = startCallContacts {
let startCall: (PeerId) -> Void = { peerId in
self.startCallWhenReady(accountId: nil, peerId: peerId, isVideo: isVideo)
}
func cleanPhoneNumber(_ text: String) -> String {
var result = ""
for c in text {
if c == "+" {
if result.isEmpty {
result += String(c)
}
} else if c >= "0" && c <= "9" {
result += String(c)
}
}
return result
}
func matchPhoneNumbers(_ lhs: String, _ rhs: String) -> Bool {
if lhs.count < 10 && lhs.count == rhs.count {
return lhs == rhs
} else if lhs.count >= 10 && rhs.count >= 10 && lhs.suffix(10) == rhs.suffix(10) {
return true
} else {
return false
}
}
if let contact = startCallContacts.first {
let contactByIdentifier: Signal<EnginePeer?, NoError>
if let context = self.contextValue?.context, let contactIdentifier = contact.contactIdentifier {
contactByIdentifier = context.engine.contacts.findPeerByLocalContactIdentifier(identifier: contactIdentifier)
} else {
contactByIdentifier = .single(nil)
}
let _ = (contactByIdentifier |> deliverOnMainQueue).start(next: { peerByContact in
var processed = false
if let peerByContact = peerByContact {
startCall(peerByContact.id)
processed = true
} else if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
if let value = Int64(string) {
startCall(PeerId(value))
processed = true
}
}
if !processed, let handle = contact.personHandle, let value = handle.value {
switch handle.type {
case .unknown:
if let value = Int64(value) {
startCall(PeerId(value))
processed = true
}
case .phoneNumber:
let phoneNumber = cleanPhoneNumber(value)
if !phoneNumber.isEmpty {
guard let context = self.contextValue?.context else {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|> map { contactList -> PeerId? in
var result: PeerId?
for peer in contactList.peers {
if case let .user(peer) = peer, let peerPhoneNumber = peer.phone {
if matchPhoneNumbers(phoneNumber, peerPhoneNumber) {
result = peer.id
break
}
}
}
return result
}
|> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
startCall(peerId)
}
})
processed = true
}
default:
break
}
}
})
return true
}
} else if let sendMessageIntent = userActivity.interaction?.intent as? INSendMessageIntent {
if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") {
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
if let value = Int64(string) {
self.openChatWhenReady(accountId: nil, peerId: PeerId(value), threadId: nil, activateInput: true, storyId: nil)
}
}
}
}
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
self.openUrl(url: url)
}
if userActivity.activityType == CSSearchableItemActionType {
if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String, uniqueIdentifier.hasPrefix("contact-") {
if let peerIdValue = Int64(String(uniqueIdentifier[uniqueIdentifier.index(uniqueIdentifier.startIndex, offsetBy: "contact-".count)...])) {
let peerId = PeerId(peerIdValue)
let signal = self.sharedContextPromise.get()
|> take(1)
|> mapToSignal { sharedApplicationContext -> Signal<(AccountRecordId?, [AccountContext?]), NoError> in
return sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> mapToSignal { primary, contexts, _ -> Signal<(AccountRecordId?, [AccountContext?]), NoError> in
return combineLatest(contexts.map { _, context, _ -> Signal<AccountContext?, NoError> in
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> map { peer -> AccountContext? in
if peer != nil {
return context
} else {
return nil
}
}
})
|> map { contexts -> (AccountRecordId?, [AccountContext?]) in
return (primary?.account.id, contexts)
}
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { primary, contexts in
if let primary = primary {
for context in contexts {
if let context = context, context.account.id == primary {
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
return
}
}
}
for context in contexts {
if let context = context {
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
return
}
}
})
}
}
}
return true
}
@available(iOS 9.0, *)
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
let _ = (self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedContext in
let type = ApplicationShortcutItemType(rawValue: shortcutItem.type)
let immediately = type == .account
let proceed: () -> Void = {
let _ = (self.context.get()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
if let context = context {
if let type = type {
switch type {
case .search:
context.openRootSearch()
case .compose:
context.openRootCompose()
case .camera:
context.openRootCamera()
case .savedMessages:
self.openChatWhenReady(accountId: nil, peerId: context.context.account.peerId, threadId: nil, storyId: nil)
case .account:
context.switchAccount()
case .appIcon:
context.openAppIcon()
}
}
}
})
}
if let appLockContext = sharedContext.sharedContext.appLockContext as? AppLockContextImpl, !immediately {
let _ = (appLockContext.isCurrentlyLocked
|> filter { !$0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
proceed()
})
} else {
proceed()
}
})
}
private func openNotificationSettingsWhenReady() {
let _ = (self.authorizedContext()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
context.openNotificationSettings()
})
}
private func startCallWhenReady(accountId: AccountRecordId?, peerId: PeerId, isVideo: Bool) {
let signal = self.sharedContextPromise.get()
|> take(1)
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext, NoError> in
if let accountId = accountId {
sharedApplicationContext.sharedContext.switchToAccount(id: accountId)
return self.authorizedContext()
|> filter { context in
context.context.account.id == accountId
}
|> take(1)
} else {
return self.authorizedContext()
|> take(1)
}
}
self.openChatWhenReadyDisposable.set((signal
|> deliverOnMainQueue).start(next: { context in
context.startCall(peerId: peerId, isVideo: isVideo)
}))
}
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) {
let signal = self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { sharedApplicationContext -> Signal<AuthorizedApplicationContext, NoError> in
if let accountId = accountId {
sharedApplicationContext.sharedContext.switchToAccount(id: accountId)
return self.authorizedContext()
|> filter { context in
context.context.account.id == accountId
}
|> take(1)
} else {
return self.authorizedContext()
|> take(1)
}
}
self.openChatWhenReadyDisposable.set((signal
|> deliverOnMainQueue).start(next: { context in
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny, alwaysKeepMessageId: alwaysKeepMessageId)
}))
}
private var openUrlInProgress: URL?
private func openUrlWhenReady(url: URL) {
self.openUrlInProgress = url
self.openUrlWhenReadyDisposable.set((self.authorizedContext()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] context in
context.openUrl(url)
Queue.mainQueue().after(1.0, {
self?.openUrlInProgress = nil
})
}))
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let _ = (accountIdFromNotification(response.notification, sharedContext: self.sharedContextPromise.get())
|> deliverOnMainQueue).start(next: { accountId in
if response.actionIdentifier == UNNotificationDefaultActionIdentifier {
if let (peerId, threadId) = peerIdFromNotification(response.notification) {
var messageId: MessageId? = nil
if response.notification.request.content.categoryIdentifier == "c" || response.notification.request.content.categoryIdentifier == "t" {
messageId = messageIdFromNotification(peerId: peerId, notification: response.notification)
}
let storyId = storyIdFromNotification(peerId: peerId, notification: response.notification)
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: threadId, messageId: messageId, storyId: storyId)
}
completionHandler()
} else if response.actionIdentifier == "reply", let (peerId, threadId) = peerIdFromNotification(response.notification), let accountId = accountId {
guard let response = response as? UNTextInputNotificationResponse, !response.userText.isEmpty else {
completionHandler()
return
}
let text = response.userText
let signal = self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { sharedContext -> Signal<Void, NoError> in
sharedContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0, extendNow: true)
return sharedContext.sharedContext.activeAccountContexts
|> mapToSignal { _, contexts, _ -> Signal<Account, NoError> in
for context in contexts {
if context.1.account.id == accountId {
return .single(context.1.account)
}
}
return .complete()
}
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { account -> Signal<Void, NoError> in
if let messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) {
let _ = TelegramEngine(account: account).messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)).start()
}
var replyToMessageId: MessageId?
if let threadId {
replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
}
return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|> map { messageIds -> MessageId? in
if messageIds.isEmpty {
return nil
} else {
return messageIds[0]
}
}
|> mapToSignal { messageId -> Signal<Void, NoError> in
if let messageId = messageId {
return account.postbox.unsentMessageIdsView()
|> filter { view in
return !view.ids.contains(messageId)
}
|> take(1)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
}
}
}
|> deliverOnMainQueue
let disposable = MetaDisposable()
disposable.set((signal
|> afterDisposed { [weak disposable] in
Queue.mainQueue().async {
if let disposable = disposable {
self.replyFromNotificationsDisposables.remove(disposable)
}
completionHandler()
}
}).start())
self.replyFromNotificationsDisposables.add(disposable)
} else {
completionHandler()
}
})
}
func requestNotificationTokenInvalidation() {
UIApplication.shared.unregisterForRemoteNotifications()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
UIApplication.shared.registerForRemoteNotifications()
})
}
private func registerForNotifications(context: AccountContextImpl, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.sharedContext.accountManager.transaction { transaction -> Bool in
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.inAppNotificationSettings)?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings
return settings.displayNameOnLockscreen
}
|> deliverOnMainQueue).start(next: { displayNames in
self.registerForNotifications(replyString: presentationData.strings.Notification_Reply, messagePlaceholderString: presentationData.strings.Conversation_InputTextPlaceholder, hiddenContentString: presentationData.strings.Watch_MessageView_Title, hiddenReactionContentString: presentationData.strings.Notification_LockScreenReactionPlaceholder, hiddenStoryContentString: presentationData.strings.Notification_LockScreenStoryPlaceholder, hiddenStoryReactionContentString: presentationData.strings.PUSH_REACT_STORY_HIDDEN, includeNames: displayNames, authorize: authorize, completion: completion)
})
}
private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, hiddenReactionContentString: String, hiddenStoryContentString: String, hiddenStoryReactionContentString: String, includeNames: Bool, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) {
let notificationCenter = UNUserNotificationCenter.current()
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
notificationCenter.getNotificationSettings(completionHandler: { settings in
Logger.shared.log("App \(self.episodeId)", "register for notifications: received settings: \(settings.authorizationStatus)")
switch (settings.authorizationStatus, authorize) {
case (.authorized, _), (.notDetermined, true):
var authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay]
if #available(iOS 12.0, *) {
authorizationOptions.insert(.providesAppNotificationSettings)
}
if #available(iOS 13.0, *) {
authorizationOptions.insert(.announcement)
}
Logger.shared.log("App \(self.episodeId)", "register for notifications: request authorization")
notificationCenter.requestAuthorization(options: authorizationOptions, completionHandler: { result, _ in
Logger.shared.log("App \(self.episodeId)", "register for notifications: received authorization: \(result)")
completion(result)
if result {
Queue.mainQueue().async {
let reply = UNTextInputNotificationAction(identifier: "reply", title: replyString, options: [], textInputButtonTitle: replyString, textInputPlaceholder: messagePlaceholderString)
let unknownMessageCategory: UNNotificationCategory
let repliableMessageCategory: UNNotificationCategory
let repliableMediaMessageCategory: UNNotificationCategory
let groupRepliableMessageCategory: UNNotificationCategory
let groupRepliableMediaMessageCategory: UNNotificationCategory
let channelMessageCategory: UNNotificationCategory
let reactionMessageCategory: UNNotificationCategory
let storyCategory: UNNotificationCategory
let storyReactionCategory: UNNotificationCategory
var options: UNNotificationCategoryOptions = []
if includeNames {
options.insert(.hiddenPreviewsShowTitle)
}
var carPlayOptions = options
carPlayOptions.insert(.allowInCarPlay)
if #available(iOS 13.2, *) {
carPlayOptions.insert(.allowAnnouncement)
}
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
reactionMessageCategory = UNNotificationCategory(identifier: "t", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenReactionContentString, options: options)
storyCategory = UNNotificationCategory(identifier: "st", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenStoryContentString, options: options)
storyReactionCategory = UNNotificationCategory(identifier: "str", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenStoryReactionContentString, options: options)
UNUserNotificationCenter.current().setNotificationCategories([
unknownMessageCategory,
repliableMessageCategory,
repliableMediaMessageCategory,
channelMessageCategory,
reactionMessageCategory,
groupRepliableMessageCategory,
groupRepliableMediaMessageCategory,
storyCategory,
storyReactionCategory
])
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
UIApplication.shared.registerForRemoteNotifications()
}
}
})
default:
break
}
})
}
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let _ = (accountIdFromNotification(notification, sharedContext: self.sharedContextPromise.get())
|> deliverOnMainQueue).start(next: { accountId in
if let context = self.contextValue {
if let accountId = accountId, context.context.account.id != accountId {
completionHandler([.alert])
}
}
})
}
@available(iOS 12.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
self.openNotificationSettingsWhenReady()
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
Logger.shared.log("App \(self.episodeId)", "handleEventsForBackgroundURLSession \(identifier)")
completionHandler()
}
private var lastCheckForUpdatesTimestamp: Double?
private let currentCheckForUpdatesDisposable = MetaDisposable()
private func maybeCheckForUpdates() {
#if targetEnvironment(simulator)
#else
guard let buildConfig = self.buildConfig, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty else {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
if self.lastCheckForUpdatesTimestamp == nil || self.lastCheckForUpdatesTimestamp! < timestamp - 10.0 * 60.0 {
self.lastCheckForUpdatesTimestamp = timestamp
if let url = URL(string: "https://api.appcenter.ms/v0.1/public/sdk/apps/\(appCenterId)/releases/latest") {
self.currentCheckForUpdatesDisposable.set((downloadHTTPData(url: url)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
return
}
guard let dict = json as? [String: Any] else {
return
}
guard let versionString = dict["version"] as? String, let version = Int(versionString) else {
return
}
guard let releaseNotesUrl = dict["release_notes_url"] as? String else {
return
}
guard let currentVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, let currentVersion = Int(currentVersionString) else {
return
}
if currentVersion < version {
let _ = (strongSelf.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { sharedContext in
let presentationData = sharedContext.sharedContext.currentPresentationData.with { $0 }
sharedContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "A new build is available", actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Show", action: {
sharedContext.sharedContext.applicationBindings.openUrl(releaseNotesUrl)
})
]), on: .root, blockInteraction: false, completion: {})
})
}
}))
}
}
#endif
}
override var next: UIResponder? {
if let context = self.contextValue, let controller = context.context.keyShortcutsController {
return controller
}
return super.next
}
@objc func debugPressed() {
let _ = (Logger.shared.collectShortLogFiles()
|> deliverOnMainQueue).start(next: { logs in
var activityItems: [Any] = []
for (_, path) in logs {
activityItems.append(URL(fileURLWithPath: path))
}
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
self.window?.rootViewController?.present(activityController, animated: true, completion: nil)
})
}
private func resetIntentsIfNeeded(context: AccountContextImpl) {
let _ = (context.sharedContext.accountManager.transaction { transaction in
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.intentsSettings)?.get(IntentsSettings.self) ?? IntentsSettings.defaultSettings
if !settings.initiallyReset || settings.account == nil {
if #available(iOS 10.0, *) {
Queue.mainQueue().async {
INInteraction.deleteAll()
}
}
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.intentsSettings, { _ in
return PreferencesEntry(IntentsSettings(initiallyReset: true, account: context.account.peerId, contacts: settings.contacts, privateChats: settings.privateChats, savedMessages: settings.savedMessages, groups: settings.groups, onlyShared: settings.onlyShared))
})
}
}).start()
}
}
private func notificationPayloadKey(data: Data) -> Data? {
if data.count < 8 {
return nil
}
return data.subdata(in: 0 ..< 8)
}
@available(iOS 10.0, *)
private func accountIdFromNotification(_ notification: UNNotification, sharedContext: Signal<SharedApplicationContext, NoError>) -> Signal<AccountRecordId?, NoError> {
if let id = notification.request.content.userInfo["accountId"] as? Int64 {
return .single(AccountRecordId(rawValue: id))
} else if let idString = notification.request.content.userInfo["accountId"] as? String, let id = Int64(idString) {
return .single(AccountRecordId(rawValue: id))
} else {
var encryptedData: Data?
if var encryptedPayload = notification.request.content.userInfo["p"] as? String {
encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+")
encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/")
while encryptedPayload.count % 4 != 0 {
encryptedPayload.append("=")
}
encryptedData = Data(base64Encoded: encryptedPayload)
}
if let encryptedData = encryptedData, let notificationKeyId = notificationPayloadKey(data: encryptedData) {
return sharedContext
|> take(1)
|> mapToSignal { sharedContext -> Signal<AccountRecordId?, NoError> in
return sharedContext.sharedContext.activeAccountContexts
|> take(1)
|> mapToSignal { _, contexts, _ -> Signal<AccountRecordId?, NoError> in
let keys = contexts.map { _, context, _ -> Signal<(AccountRecordId, MasterNotificationKey)?, NoError> in
return masterNotificationsKey(account: context.account, ignoreDisabled: true)
|> map { key in
return (context.account.id, key)
}
}
return combineLatest(keys)
|> map { keys -> AccountRecordId? in
for idAndKey in keys {
if let (id, key) = idAndKey, key.id == notificationKeyId {
return id
}
}
return nil
}
}
}
} else if let userId = notification.request.content.userInfo["userId"] as? Int {
return sharedContext
|> take(1)
|> mapToSignal { sharedContext -> Signal<AccountRecordId?, NoError> in
return sharedContext.sharedContext.activeAccountContexts
|> take(1)
|> map { _, contexts, _ -> AccountRecordId? in
for (_, context, _) in contexts {
if Int(context.account.peerId.id._internalGetInt64Value()) == userId {
return context.account.id
}
}
return nil
}
}
} else {
return .single(nil)
}
}
}
@available(iOS 10.0, *)
private func peerIdFromNotification(_ notification: UNNotification) -> (peerId: PeerId, threadId: Int64?)? {
let threadId = notification.request.content.userInfo["threadId"] as? Int64
if let peerId = notification.request.content.userInfo["peerId"] as? Int64 {
return (PeerId(peerId), threadId)
} else if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerId = Int64(peerIdString) {
return (PeerId(peerId), threadId)
} else {
let payload = notification.request.content.userInfo
var peerId: PeerId?
if let fromId = payload["from_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
} else if let fromId = payload["chat_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
} else if let fromId = payload["channel_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
} else if let fromId = payload["encryption_id"] {
let fromIdValue = fromId as! NSString
peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(fromIdValue as String) ?? 0))
}
if let peerId = peerId {
return (peerId, threadId)
} else {
return nil
}
}
}
private func messageIdFromNotification(peerId: PeerId, notification: UNNotification) -> MessageId? {
let payload = notification.request.content.userInfo
if let messageIdNamespace = payload["messageId.namespace"] as? Int32, let messageIdId = payload["messageId.id"] as? Int32 {
return MessageId(peerId: peerId, namespace: messageIdNamespace, id: messageIdId)
}
if let msgId = payload["msg_id"] {
let msgIdValue = msgId as! NSString
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue))
}
return nil
}
private func storyIdFromNotification(peerId: PeerId, notification: UNNotification) -> StoryId? {
let payload = notification.request.content.userInfo
if let storyId = payload["story_id"] {
let storyIdValue = storyId as! NSString
return StoryId(peerId: peerId, id: Int32(storyIdValue.intValue))
}
return nil
}
private enum DownloadFileError {
case network
}
private func downloadHTTPData(url: URL) -> Signal<Data, DownloadFileError> {
return Signal { subscriber in
let completed = Atomic<Bool>(value: false)
let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, _, error in
let _ = completed.swap(true)
if let location = location, let data = try? Data(contentsOf: location) {
subscriber.putNext(data)
subscriber.putCompletion()
} else {
subscriber.putError(.network)
}
})
downloadTask.resume()
return ActionDisposable {
if !completed.with({ $0 }) {
downloadTask.cancel()
}
}
}
}
private func getMemoryConsumption() -> Int {
guard let memory_offset = MemoryLayout.offset(of: \task_vm_info_data_t.min_address) else {
return 0
}
let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size)
let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(memory_offset / MemoryLayout<integer_t>.size)
var info = task_vm_info_data_t()
var count = TASK_VM_INFO_COUNT
let kr = withUnsafeMutablePointer(to: &info) { infoPtr in
infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count)
}
}
guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else {
return 0
}
return Int(info.phys_footprint)
}