Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  RootCoordinator.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Coordinator
import Keychain
import SwiftUI
import ThreemaEssentials
import ThreemaFramework
import ThreemaMacros

final class RootCoordinator: Coordinator {
    var childCoordinators: [any Coordinator] = []
    
    var appCoordinator: AppCoordinator? {
        childCoordinators.first(where: {
            $0 is AppCoordinator
        }) as? AppCoordinator
    }
    
    var tabBarController: UITabBarController? {
        appCoordinator?.tabBarController
    }

    private var onboardingCoordinator: OnboardingCoordinator? {
        childCoordinators.first(where: {
            $0 is OnboardingCoordinator
        }) as? OnboardingCoordinator
    }

    /// Whether a lock is owed at the next activation. `sceneDidBecomeActive` also fires for interruptions
    /// that never backgrounded the scene, and those must not re-lock the app.
    private var didLeaveForegroundSinceUnlock = false

    private var passcodeCoordinator: PasscodeCoordinator? {
        childCoordinators.lazy.compactMap {
            $0 as? PasscodeCoordinator
        }.first
    }
    
    private lazy var rootNavigationController: UINavigationController = {
        let navigationController = UINavigationController()
        navigationController.isNavigationBarHidden = true
        return navigationController
    }()
    
    private lazy var loadingCoordinator = LoadingCoordinator(
        viewModel: LoadingViewModel(),
        presentingNavigationViewController: rootNavigationController,
        window: window
    )
    
    private lazy var callLauncher = CallLauncher(
        voipCallStateManager: VoIPCallStateManager.shared,
        groupCallSingleton: GlobalGroupCallManagerSingleton.shared,
        windowScene: windowScene
    )
    
    var rootViewController: UIViewController {
        rootNavigationController
    }
    
    private let window: UIWindow
    private let windowScene: UIWindowScene
    private let bootstrap: BootstrapContainer
    private let launchManager: AppLaunchSequenceManager
    private var screenshotObservers: [any NSObjectProtocol] = []

    private(set) lazy var errorNotificationHandler = ErrorNotificationHandler()

    /// Set when the launch sequence dead-ends because protected data (keychain) was
    /// unavailable — a cold launch while the device is locked. The pre-coordinator
    /// app retried in `applicationProtectedDataDidBecomeAvailable`; without a retry
    /// the app stays on the launch screen forever once the user opens it.
    private var isAwaitingProtectedData = false

    /// License activation link that arrived before the launch sequence reached its destination.
    /// Consumed by ``goToOnboarding()`` and ``goToApp(appContainer:)``, because only they know
    /// whether onboarding or the app has to handle it.
    private var pendingLicenseURL: URL?

    /// Strong owner of the intent handler. ``IntentHandlerProxy`` (via ``DeferredDispatcher``) only
    /// holds it weakly, so without this reference it would deallocate right after
    /// ``goToApp(appContainer:)`` returns and every queued shortcut/intent would silently never fire.
    /// Reassigned on every ``goToApp(appContainer:)`` so it always tracks the current `AppCoordinator`.
    private var intentHandler: IntentHandler?

    private var isSceneForeground: Bool {
        windowScene.activationState != .background
    }
    
    deinit {
        for observer in screenshotObservers {
            NotificationCenter.default.removeObserver(observer)
        }
    }

    init(
        window: UIWindow,
        windowScene: UIWindowScene,
        bootstrap: BootstrapContainer
    ) {
        self.window = window
        self.windowScene = windowScene
        self.bootstrap = bootstrap
        self.launchManager = AppLaunchSequenceManager(bootstrap: bootstrap)
    }

    func start() {
        window.makeSecure()
        
        childCoordinators.append(loadingCoordinator)
        loadingCoordinator.start()
        
        registerScreenshotDetectionObservers()

        errorNotificationHandler.startObserving()

        Task { @MainActor [weak self] in
            await self?.performLaunchSequence()
        }
    }
    
    // MARK: - Scene Lifecycle

    func sceneWillEnterForeground() {
        retryLaunchIfProtectedDataBecameAvailable()
        
        if let appCoordinator {
            let appLaunchTasks = AppLaunchTasks(businessInjector: appCoordinator.appContainer.businessInjector)
            appLaunchTasks.run(launchEvent: .willEnterForeground)
        }
        
        appCoordinator?.willEnterForeground()
        
        // Reload pending user notification cache, because could be changed by Notification Extension in the mean time
        appCoordinator?.appContainer.incomingMessageManager.reloadPendingUserNotificationCache()

        requestReauthenticationIfNeeded()
        hidePrivacyOverlay()
        reconcileTypingObservation()
    }

    func sceneDidBecomeActive() {
        hidePrivacyOverlay()
        appCoordinator?.appContainer.notificationManager.updateUnreadMessagesCount()
        appCoordinator?.didBecomeActive()

        if let passcodeCoordinator {
            passcodeCoordinator.retryBiometricAuthenticationIfNeeded()
        }
        else if didLeaveForegroundSinceUnlock {
            requestReauthenticationIfNeeded()
        }

        reconcileTypingObservation()
    }

    func sceneWillResignActive() {
        appCoordinator?.appContainer.notificationHandler.resetFirstPushHandled()
        showPrivacyOverlay()
    }

    func sceneDidEnterBackground() {
        didLeaveForegroundSinceUnlock = true
        passcodeCoordinator?.sceneDidEnterBackground()
        appCoordinator?.appContainer.passcodeLock.updateLastUnlockTime()
        appCoordinator?.appContainer.bgTaskManager.scheduleTasks()

        appCoordinator?.handleSceneDidEnterBackground()

        SettingsBundleHelper.resetSafeMode()

        showPrivacyOverlay()
        reconcileTypingObservation()

        // Drain pending log writes while we are still running. If the process is suspended with a write in flight we
        // lose the messages and risk a `0xdead10cc` termination for holding a lock in the app group container.
        LogManager.flushLogs()
    }

    // MARK: - Re-authentication

    func requestReauthenticationIfNeeded() {
        // A lock that is up cannot be replaced, but it may still owe the user a biometric prompt.
        if let passcodeCoordinator {
            passcodeCoordinator.retryBiometricAuthenticationIfNeeded()
            return
        }

        guard let appContainer = appCoordinator?.appContainer else {
            return
        }

        let passcodeLock = appContainer.passcodeLock
        guard
            passcodeLock.isPasscodeRequired(),
            !passcodeLock.isWithinGracePeriod()
        else {
            return
        }

        let passcodeCoordinator = PasscodeCoordinator(
            windowScene: windowScene,
            appContainer: appContainer,
            delegate: self
        )
        childCoordinators.append(passcodeCoordinator)
        passcodeCoordinator.start()
    }

    private func reconcileTypingObservation() {
        guard let manager = appCoordinator?.appContainer.typingIndicatorManager else {
            return
        }
        if isSceneForeground {
            manager.startObserving()
        }
        else {
            manager.stopObserving()
        }
    }

    // MARK: - Privacy Overlay

    func showPrivacyOverlay() {
        guard !childCoordinators.contains(where: { $0 is PrivacyCoordinator }) else {
            return
        }
        let coordinator = PrivacyCoordinator(
            windowScene: windowScene,
            isPasscodeRequired: { [weak self] in
                self?.appCoordinator?.appContainer.passcodeLock.isPasscodeRequired() == true
            },
            isRemoteSecretEnabled: {
                do {
                    return try RemoteSecretProvider.isRemoteSecretInKeychain()
                }
                catch {
                    DDLogError("Unable to fetch remote secret from keychain: \(error)")
                    return true
                }
            }
        )
        childCoordinators.append(coordinator)
        coordinator.start()
    }

    func hidePrivacyOverlay() {
        guard let coordinator = childCoordinators.first(where: {
            $0 is PrivacyCoordinator
        }) as? PrivacyCoordinator else {
            return
        }
        coordinator.hideOverlay()
        childDidFinish(coordinator)
    }

    // MARK: - Screenshot Detection

    private func registerScreenshotDetectionObservers() {
        guard bootstrap.flavorService.isBusinessApp else {
            return
        }

        screenshotObservers.append(NotificationCenter.default.addObserver(
            forName: UIApplication.userDidTakeScreenshotNotification,
            object: nil,
            queue: .main,
            using: { [weak self] _ in
                Task { @MainActor in
                    self?.showScreenshotPrevention()
                }
            }
        ))

        screenshotObservers.append(NotificationCenter.default.addObserver(
            forName: UIScreen.capturedDidChangeNotification,
            object: nil,
            queue: .main,
            using: { [weak self] _ in
                Task { @MainActor in
                    guard UIScreen.main.isCaptured else {
                        return
                    }
                    self?.showScreenshotPrevention()
                }
            }
        ))
    }

    private func showScreenshotPrevention() {
        guard
            bootstrap.flavorService.isBusinessApp,
            MDMSetup().disableScreenshots()
        else {
            return
        }

        let hostingController = UIHostingController(rootView: ScreenshotPreventionView())
        let screenshotPreventionView = hostingController.view!
        screenshotPreventionView.translatesAutoresizingMaskIntoConstraints = false
        window.addSubview(screenshotPreventionView)

        NSLayoutConstraint.activate([
            screenshotPreventionView.topAnchor.constraint(equalTo: window.topAnchor),
            screenshotPreventionView.bottomAnchor.constraint(equalTo: window.bottomAnchor),
            screenshotPreventionView.leadingAnchor.constraint(equalTo: window.leadingAnchor),
            screenshotPreventionView.trailingAnchor.constraint(equalTo: window.trailingAnchor),
        ])

        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [hostingController] in
            hostingController.view.removeFromSuperview()
        }
    }

    // MARK: - License Activation Link

    /// Handles a license activation link (`<scheme>://license?…`)
    ///
    /// License links must also work before the app is set up, so they can't go through
    /// ``IntentHandlerProxy``, which only delivers once the app UI exists. Forwards to the
    /// ``OnboardingCoordinator`` while onboarding runs, buffers the link while the launch sequence is
    /// still deciding where to go, and otherwise handles it in the running app.
    func handleLicenseURL(_ url: URL) {
        guard url.host()?.lowercased() == "license" else {
            DDLogError(
                "[RootCoordinator] Failed to handle URL: Not a license URL (\(url.host() ?? "unknown")). Aborting license handling."
            )
            return
        }

        guard bootstrap.flavorService.isBusinessApp else {
            DDLogError(
                "[RootCoordinator] Failed to handle URL: No license for private app. Aborting license handling."
            )
            return
        }

        if let onboardingCoordinator {
            onboardingCoordinator.handleLicenseURL(query: url.getQueries)
        }
        else if appCoordinator != nil {
            URLHandler.handleLicense(from: url)
        }
        else {
            pendingLicenseURL = url
        }
    }

    // MARK: - Launch Sequence

    @MainActor
    private func performLaunchSequence() async {
        isAwaitingProtectedData = false
        loadingCoordinator.showLoading()

        bootstrap.appLaunchManager.runLaunchSetup(window: window)

        let result = await launchManager.run()
        await handleLaunchResult(result)
    }

    private func handleLaunchResult(_ result: AppLaunchSequenceManager.LaunchResult) async {
        switch result {
        case .needsOnboarding:
            await goToOnboarding()

        case let .needsPasscode(appContainer):
            await goToPasscode(appContainer: appContainer)

        case .needsRemoteSecretFetch:
            await handleRemoteSecretFetch()

        case .protectedDataUnavailable:
            isAwaitingProtectedData = true
            showProtectedDataUnavailable()

        case let .ready(appContainer):
            await goToApp(appContainer: appContainer)

        case let .failed(error):
            showError(error)
        }
    }

    /// Re-runs the launch sequence if it previously dead-ended on locked protected
    /// data. Called when protected data becomes available and when the scene comes
    /// to the foreground (the launch may have happened in the background, e.g. for
    /// a background task, while the device was still locked).
    func retryLaunchIfProtectedDataBecameAvailable() {
        guard isAwaitingProtectedData, !bootstrap.bootstrapKeychainManager.isKeychainLocked else {
            return
        }

        // Reset before the task runs so a second trigger (foreground + protected
        // data notification) doesn't start the launch sequence twice.
        isAwaitingProtectedData = false

        Task { @MainActor [weak self] in
            await self?.performLaunchSequence()
        }
    }
    
    // MARK: - Transitions
    
    private func goToOnboarding() async {
        let bootstrapMDMSetup = BootstrapMDMAdapter()

        let remoteSecretResolver = RemoteSecretResolver(
            appLaunchManager: bootstrap.appLaunchManager,
            licenseStore: bootstrap.licenseStore.store,
            myIdentityStore: bootstrap.bootstrapIdentityStore.store,
            isRemoteSecretEnabledByMDM: { [bootstrapMDMSetup] in
                bootstrapMDMSetup.isRemoteSecretEnabled
            },
            flavorService: AppFlavorService(),
            hasPreexistingData: bootstrap.appLaunchManager.hasPreexistingDatabaseFile
        )
        let onboardingCoordinator = OnboardingCoordinator(
            bootstrap: bootstrap,
            bootstrapMDMSetup: bootstrapMDMSetup,
            delegate: self,
            window: window,
            remoteSecretResolver: remoteSecretResolver
        )
        
        childCoordinators.append(onboardingCoordinator)
        onboardingCoordinator.start()

        // Hand the link over before the onboarding flow starts (splash `viewDidAppear`), so its
        // license check validates the credentials of the link instead of the ones stored before.
        if let pendingLicenseURL {
            self.pendingLicenseURL = nil
            onboardingCoordinator.handleLicenseURL(query: pendingLicenseURL.getQueries)
        }

        childDidFinish(loadingCoordinator)
    }
    
    private func goToPasscode(appContainer: AppDependencyContainer) async {
        configureCallProvider()

        let passcodeCoordinator = PasscodeCoordinator(
            windowScene: windowScene,
            appContainer: appContainer,
            delegate: self
        )
        childCoordinators.append(passcodeCoordinator)
        passcodeCoordinator.start()
        childDidFinish(loadingCoordinator)
    }
    
    /// Presents RemoteSecret fetch UI (spinner + error recovery) on the loading
    /// coordinator's navigation controller, then continues the launch sequence.
    private func handleRemoteSecretFetch() async {
        let navigationController = loadingCoordinator.presentingNavigationViewController
        
        let viewsManager = RemoteSecretInitializeViewsManager(
            navigationController: navigationController,
            showDeleteAfterRetries: 0
        )
        
        do {
            let identity = bootstrap.bootstrapIdentityStore.store.identity
                .map { ThreemaIdentity($0) }
            
            let remoteSecretManager = try await viewsManager.start(
                identity: identity,
                onDelete: { [weak self] in
                    try? self?.bootstrap.bootstrapKeychainManager.deleteAllItems()
                    exit(EXIT_SUCCESS)
                },
                onCancel: nil
            )
            
            // Restore loading view after fetch UI
            loadingCoordinator.showLoading()
            
            let result = await launchManager.continueAfterRemoteSecretFetch(
                remoteSecretManager: remoteSecretManager
            )
            await handleLaunchResult(result)
        }
        catch {
            loadingCoordinator.showLoading()
            showError(
                LaunchError.remoteSecretSetupFailed(
                    LaunchRemoteSecretError(
                        code: .general(error),
                        enableDeletion: false
                    )
                )
            )
        }
    }
    
    private func configureCallProvider() {
        CallProvider.shared.setCallLauncher(callLauncher)
    }

    private func goToApp(appContainer: AppDependencyContainer) async {
        configureCallProvider()

        // Tear down a previous AppCoordinator (e.g. after erase → onboarding → relaunch).
        // The `appCoordinator` accessor scans `childCoordinators` by type, so a stale
        // instance left in there would shadow or race the new one.
        if let previousAppCoordinator = appCoordinator {
            previousAppCoordinator.finish()
            childDidFinish(previousAppCoordinator)
        }

        let appCoordinator = AppCoordinator(
            window: window,
            appContainer: appContainer,
            launchModalManager: LaunchModalManager.shared
        )
        childCoordinators.append(appCoordinator)

        // NOTE: Do NOT attach the split view to the window here. `appCoordinator.start()` configures the
        // split view's columns/tabs and THEN sets it as the window root and calls `makeKeyAndVisible()`.
        // Attaching + showing the still-unconfigured split view first caused a second `makeKeyAndVisible`
        // / root swap, which left the `.doubleColumn` split view compact-but-NOT-collapsed on launch (the
        // empty logo screen that only a real size-class transition / rotation could recover).
        appCoordinator.start()

        childDidFinish(loadingCoordinator)

        reconcileTypingObservation()
        let handler = makeIntentHandler(appContainer: appContainer, appCoordinator: appCoordinator)
        intentHandler = handler
        SceneDelegate.current?.intentHandlerProxy.attach(handler)

        // A license link received during launch (e.g. relicensing an already set up app): the
        // running-app path needs the app UI, so it can only run now.
        if let pendingLicenseURL {
            self.pendingLicenseURL = nil
            URLHandler.handleLicense(from: pendingLicenseURL)
        }

        // Attach the notification handler only now: replaying a queued cold-start
        // notification response (push tap) any earlier deep-links into a UI that is
        // still being built. The hop to the next main-actor turn lets the first
        // frame of the app hierarchy commit (and the tab routers bind in
        // `viewIsAppearing`) before the deep link mutates navigation state.
        Task { @MainActor in
            ApplicationDelegate.delegate?.notificationDelegateProxy
                .attach(appContainer.notificationHandler)
        }

        /// Start Sentry now that all dependencies are ready,
        /// due to our redaction mechanism depending on the entity manager,
        /// and the entity manager, on remote secret.
        SentryClient.shared.start()
    }

    private func makeIntentHandler(
        appContainer: AppDependencyContainer,
        appCoordinator: AppCoordinator
    ) -> IntentHandler {
        IntentHandler(
            entityManager: appContainer.businessInjector.entityManager,
            voipCallStateManager: .shared,
            hasAudioCallSupportForContact: {
                FeatureMask.check(
                    contact: Contact(contactEntity: $0),
                    for: .o2OAudioCallSupport
                )
            },
            handleConversation: { [weak appCoordinator] conversation in
                appCoordinator?.show(conversation: conversation)
            },
            rootViewControllerProvider: { [weak self] in
                self?.window.rootViewController
            },
            urlHandlerFactory: {
                URLHandler(businessInjector: appContainer.businessInjector)
            }
        )
    }
    
    private func showProtectedDataUnavailable() {
        loadingCoordinator.showError(
            message: #localize("root_coordinator_protected_data_unavailable_message"),
            onRetry: { [weak self] in
                Task {
                    await self?.performLaunchSequence()
                }
            }
        )
    }
    
    private func showError(_ error: LaunchError) {
        // Needs to be separate, because inlining it below leads to a compiler error
        let onRetry: (@Sendable () -> Void) = { [weak self] in
            Task {
                await self?.performLaunchSequence()
            }
        }
        
        let onDelete: (@Sendable () -> Void) = {
            Task { @MainActor in
                DeleteRevokeIdentityManager.deleteLocalDataWithoutBusinessReady()
                exit(EXIT_SUCCESS)
            }
        }
        
        switch error {
        case .databaseMigrationFailed, .encryptedDataDetected, .keychainError, .appDelegateNotAvailable:
            loadingCoordinator.showError(
                message: error.localizedDescription,
                onRetry: error.isRetryable
                    ? onRetry
                    : nil
            )
        case let .remoteSecretSetupFailed(remoteSecretError):
            showRemoteSecretError(remoteSecretError, onRetry: onRetry, onDelete: onDelete)
        }
    }
    
    private func showRemoteSecretError(
        _ remoteSecretError: LaunchRemoteSecretError,
        onRetry: @escaping (@Sendable () -> Void),
        onDelete: @escaping (@Sendable () -> Void)
    ) {
        switch remoteSecretError.code {
        case .blocked:
            loadingCoordinator.showLocked()
        case .mismatch, .timeout:
            loadingCoordinator.showError(
                title: remoteSecretError.localizedTitle,
                message: remoteSecretError.localizedDescription,
                onRetry: remoteSecretError.isRetryable ? onRetry : nil,
                onRemove: remoteSecretError.enableDeletion ? onDelete : nil
            )
        case let .general(generalError):
            loadingCoordinator.showError(
                message: generalError.localizedDescription,
                onRetry: remoteSecretError.isRetryable ? onRetry : nil,
                onRemove: remoteSecretError.enableDeletion ? onDelete : nil
            )
        }
    }
}

// MARK: - PasscodeCoordinatorDelegate

extension RootCoordinator: PasscodeCoordinatorDelegate {

    func passcodeCoordinatorDidAuthenticate(
        _ coordinator: PasscodeCoordinator,
        appContainer: AppDependencyContainer,
        evaluatedPolicyDomainState: Data?
    ) {
        if let state = evaluatedPolicyDomainState {
            appContainer.businessInjector.userSettings.evaluatedPolicyDomainStateApp = state
        }

        // Unlocking has to complete within this main-actor turn, together with the window release in
        // `reportSuccess()`. Deferring any of it leaves a turn in which a coordinator that presents
        // nothing still counts as the app's lock.
        childDidFinish(coordinator)
        didLeaveForegroundSinceUnlock = false

        if appCoordinator != nil {
            // Re-auth: passcode window is gone, restore main window as key.
            window.makeKeyAndVisible()
        }
        else {
            // Launch-time auth: AppCoordinator doesn't exist yet and building it is async.
            Task { @MainActor [weak self] in
                await self?.goToApp(appContainer: appContainer)
            }
        }
    }

    func passcodeCoordinatorDidRequestErase(_ coordinator: PasscodeCoordinator) {
        Task { @MainActor in
            DeleteRevokeIdentityManager.deleteLocalDataWithoutBusinessReady()
        }
    }
}

// MARK: - OnboardingCoordinatorDelegate

extension RootCoordinator: OnboardingCoordinatorDelegate {
    
    func onboardingDidComplete(_ coordinator: OnboardingCoordinator, appContainer: AppDependencyContainer) {
        Task { [weak self] in
            guard let self else {
                return
            }
            
            await goToApp(appContainer: appContainer)

            checkHasPrivateChats(appContainer: appContainer)

            childDidFinish(coordinator)
        }
    }

    /// After onboarding (e.g. restore from backup), if private chats exist but no passcode
    /// is set, prompt the user to create one.
    private func checkHasPrivateChats(appContainer: AppDependencyContainer) {
        let passcodeLock = appContainer.passcodeLock
        guard !passcodeLock.isPasscodeRequired() else {
            return
        }

        let entityManager = appContainer.businessInjector.entityManager
        let hasPrivateChats = entityManager.performAndWait {
            guard let conversations = entityManager.entityFetcher.conversationEntities() else {
                return false
            }
            return conversations.contains { $0.conversationCategory == .private }
        }

        guard hasPrivateChats else {
            return
        }

        guard let rootVC = window.rootViewController else {
            return
        }

        UIAlertTemplate.showAlert(
            owner: rootVC,
            title: #localize("privateChat_alert_title"),
            message: String.localizedStringWithFormat(
                #localize("privateChat_setup_alert_message"),
                TargetManager.localizedAppName
            ),
            titleOk: #localize("privateChat_code_alert_confirm"),
            actionOk: { [weak self] _ in
                let vc = JKLLockScreenViewController(
                    nibName: NSStringFromClass(JKLLockScreenViewController.self),
                    bundle: BundleUtil.frameworkBundle()
                )
                vc.lockScreenMode = .new

                let nav = ModalNavigationController(rootViewController: vc)
                nav.isNavigationBarHidden = true
                nav.modalPresentationStyle = .fullScreen
                self?.window.rootViewController?.present(nav, animated: true)
            },
            titleCancel: nil,
            actionCancel: { _ in }
        )
    }
}

Messung
CHZG
829610092

¤ Dauer der Verarbeitung: 0.8 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104845
#Domains=499267