tdf#128502: Chunk of work to enable "multi-tasking" in the iOS app

Seems to not cause any serious regressions in the iOS app or in "make
run", but of course I am not able to run a comprehensive check of all
functionality.

Change-Id: I44a0e8d60bdbc0a885db88475961575c5e95ce88
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/93037
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Tor Lillqvist <tml@collabora.com>
diff --git a/Makefile.am b/Makefile.am
index ca868c8..171a07e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -93,6 +93,7 @@
                 common/Session.cpp \
                 common/Seccomp.cpp \
                 common/MessageQueue.cpp \
                 common/MobileApp.cpp \
                 common/SigUtil.cpp \
                 common/SpookyV2.cpp \
                 common/Unit.cpp \
@@ -250,6 +251,7 @@
                 common/Authorization.hpp \
                 common/MessageQueue.hpp \
                 common/Message.hpp \
                 common/MobileApp.hpp \
                 common/Png.hpp \
                 common/Rectangle.hpp \
                 common/SigUtil.hpp \
diff --git a/android/lib/src/main/cpp/androidapp.cpp b/android/lib/src/main/cpp/androidapp.cpp
index a3f8471..7570f96 100644
--- a/android/lib/src/main/cpp/androidapp.cpp
+++ b/android/lib/src/main/cpp/androidapp.cpp
@@ -225,10 +225,6 @@
                                       // is saved by closing it.
                                       fakeSocketClose(closeNotificationPipeForForwardingThread[1]);

                                       // Flag to make the inter-thread plumbing in the Online
                                       // bits go away quicker.
                                       MobileTerminationFlag = true;

                                       // Close our end of the fake socket connection to the
                                       // ClientSession thread, so that it terminates
                                       fakeSocketClose(currentFakeClientFd);
diff --git a/common/MobileApp.cpp b/common/MobileApp.cpp
new file mode 100644
index 0000000..1570621
--- /dev/null
+++ b/common/MobileApp.cpp
@@ -0,0 +1,46 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <cassert>
#include <map>
#include <mutex>

#include "MobileApp.hpp"

#if MOBILEAPP

static std::map<unsigned, DocumentData> idToDocDataMap;
static std::mutex idToDocDataMapMutex;

DocumentData &allocateDocumentDataForMobileAppDocId(unsigned docId)
{
    const std::lock_guard<std::mutex> lock(idToDocDataMapMutex);

    assert(idToDocDataMap.find(docId) == idToDocDataMap.end());
    idToDocDataMap[docId] = DocumentData();
    return idToDocDataMap[docId];
}

DocumentData &getDocumentDataForMobileAppDocId(unsigned docId)
{
    const std::lock_guard<std::mutex> lock(idToDocDataMapMutex);

    assert(idToDocDataMap.find(docId) != idToDocDataMap.end());
    return idToDocDataMap[docId];
}

void deallocateDocumentDataForMobileAppDocId(unsigned docId)
{
    assert(idToDocDataMap.find(docId) != idToDocDataMap.end());
    idToDocDataMap.erase(docId);
}

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/common/MobileApp.hpp b/common/MobileApp.hpp
new file mode 100644
index 0000000..fc81611
--- /dev/null
+++ b/common/MobileApp.hpp
@@ -0,0 +1,57 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#pragma once

#include "config.h"

#if MOBILEAPP

#include <LibreOfficeKit/LibreOfficeKit.hxx>

#ifdef IOS
#import "CODocument.h"
#endif

// On iOS at least we want to be able to have several documents open in the same app process.

// It is somewhat complicated to make sure we access the same LibreOfficeKit object for the document
// in both the iOS-specific Objective-C++ code and in the mostly generic Online C++ code.

// We pass around a numeric ever-increasing document identifier that gets biumped for each document
// the system asks the app to open.

// For iOS, it is the static std::atomic<unsigned> appDocIdCounter in CODocument.mm.

// In practice it will probably be equivalent to the DocumentBroker::DocBrokerId or the number that
// the core SfxViewShell::GetDocId() returns, but there might be situations where multi-threading
// and opening of several documents in sequence very quickly might cause discrepancies, so it is
// better to usea different counter to be sure. Patches to use just one counter welcome.

struct DocumentData
{
    lok::Document *loKitDocument;
#ifdef IOS
    CODocument *coDocument;
#endif

    DocumentData() :
        loKitDocument(nullptr)
#ifdef IOS
        , coDocument(nil)
#endif
    {
    }
};

DocumentData &allocateDocumentDataForMobileAppDocId(unsigned docId);
DocumentData &getDocumentDataForMobileAppDocId(unsigned docId);
void deallocateDocumentDataForMobileAppDocId(unsigned docId);

#endif
diff --git a/common/SigUtil.cpp b/common/SigUtil.cpp
index 4891010..dff88b8 100644
--- a/common/SigUtil.cpp
+++ b/common/SigUtil.cpp
@@ -38,14 +38,10 @@
#include "Common.hpp"
#include "Log.hpp"

#ifndef IOS
static std::atomic<bool> TerminationFlag(false);
static std::atomic<bool> DumpGlobalState(false);
#if MOBILEAPP
std::atomic<bool> MobileTerminationFlag(false);
#else
// Mobile defines its own, which is constexpr.
static std::atomic<bool> ShutdownRequestFlag(false);
#endif

namespace SigUtil
{
@@ -71,6 +67,7 @@
    }
#endif

#if !MOBILEAPP
    bool getDumpGlobalState()
    {
        return DumpGlobalState;
@@ -80,11 +77,7 @@
    {
        DumpGlobalState = false;
    }
}

#if !MOBILEAPP
namespace SigUtil
{
    /// This traps the signal-handler so we don't _Exit
    /// while dumping stack trace. It's re-entrant.
    /// Used to safely increment and decrement the signal-handler trap.
@@ -384,8 +377,9 @@

        return false;
    }
#endif // !MOBILEAPP
}

#endif // !MOBILEAPP
#endif // !IOS

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/common/SigUtil.hpp b/common/SigUtil.hpp
index d3eaf97..9cc5b69 100644
--- a/common/SigUtil.hpp
+++ b/common/SigUtil.hpp
@@ -13,13 +13,9 @@
#include <mutex>
#include <signal.h>

#if MOBILEAPP
static constexpr bool ShutdownRequestFlag(false);
extern std::atomic<bool> MobileTerminationFlag;
#endif

namespace SigUtil
{
#ifndef IOS
    /// Get the flag used to commence clean shutdown.
    /// requestShutdown() is used to set the flag.
    bool getShutdownRequestFlag();
@@ -33,14 +29,29 @@
    /// Only necessary in Mobile.
    void resetTerminationFlag();
#endif
#else
    // In the mobile apps we have no need to shut down the app.
    inline constexpr bool getShutdownRequestFlag()
    {
        return false;
    }

    inline constexpr bool getTerminationFlag()
    {
        return false;
    }

    inline void setTerminationFlag()
    {
    }
#endif

#if !MOBILEAPP
    /// Get the flag to dump internal state.
    bool getDumpGlobalState();
    /// Reset the flag to dump internal state.
    void resetDumpGlobalState();

#if !MOBILEAPP

    /// Wait for the signal handler, if any,
    /// and prevent _Exit while collecting backtrace.
    void waitSigHandlerTrap();
diff --git a/ios/Mobile.xcodeproj/project.pbxproj b/ios/Mobile.xcodeproj/project.pbxproj
index bedc02c..fbdb3e1 100644
--- a/ios/Mobile.xcodeproj/project.pbxproj
+++ b/ios/Mobile.xcodeproj/project.pbxproj
@@ -66,6 +66,8 @@
		BEBF3EB0246EB1C800415E87 /* RequestDetails.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BEBF3EAF246EB1C800415E87 /* RequestDetails.cpp */; };
		BECD984024336DD400016117 /* device-mobile.css in Resources */ = {isa = PBXBuildFile; fileRef = BECD983E24336DD400016117 /* device-mobile.css */; };
		BECD984124336DD400016117 /* device-tablet.css in Resources */ = {isa = PBXBuildFile; fileRef = BECD983F24336DD400016117 /* device-tablet.css */; };
		BEDCC84E2452F82800FB02BD /* MobileApp.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BEDCC84C2452F82800FB02BD /* MobileApp.cpp */; };
		BEDCC8992456FFAD00FB02BD /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BEDCC8982456FFAC00FB02BD /* SceneDelegate.m */; };
		BEFB1EE121C29CC70081D757 /* L10n.mm in Sources */ = {isa = PBXBuildFile; fileRef = BEFB1EE021C29CC70081D757 /* L10n.mm */; };
/* End PBXBuildFile section */

@@ -1176,6 +1178,10 @@
		BECBD41423D9C98500DA5582 /* svddrgmt.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = svddrgmt.cxx; path = "../../ios-device/svx/source/svdraw/svddrgmt.cxx"; sourceTree = "<group>"; };
		BECD983E24336DD400016117 /* device-mobile.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = "device-mobile.css"; path = "../../../loleaflet/dist/device-mobile.css"; sourceTree = "<group>"; };
		BECD983F24336DD400016117 /* device-tablet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = "device-tablet.css"; path = "../../../loleaflet/dist/device-tablet.css"; sourceTree = "<group>"; };
		BEDCC84C2452F82800FB02BD /* MobileApp.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MobileApp.cpp; sourceTree = "<group>"; };
		BEDCC84D2452F82800FB02BD /* MobileApp.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MobileApp.hpp; sourceTree = "<group>"; };
		BEDCC8972456FFAC00FB02BD /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
		BEDCC8982456FFAC00FB02BD /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
		BEDCC943246175E100FB02BD /* sessionlistener.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sessionlistener.cxx; path = "../../ios-device/framework/source/services/sessionlistener.cxx"; sourceTree = "<group>"; };
		BEDCC944246175E100FB02BD /* substitutepathvars.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = substitutepathvars.cxx; path = "../../ios-device/framework/source/services/substitutepathvars.cxx"; sourceTree = "<group>"; };
		BEDCC945246175E100FB02BD /* pathsettings.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = pathsettings.cxx; path = "../../ios-device/framework/source/services/pathsettings.cxx"; sourceTree = "<group>"; };
@@ -1885,6 +1891,8 @@
				BE58E129217F295B00249358 /* Log.hpp */,
				BE5EB5BD213FE29900E0826C /* MessageQueue.cpp */,
				BE58E12D217F295B00249358 /* MessageQueue.hpp */,
				BEDCC84C2452F82800FB02BD /* MobileApp.cpp */,
				BEDCC84D2452F82800FB02BD /* MobileApp.hpp */,
				BE58E12A217F295B00249358 /* Png.hpp */,
				BE5EB5BF213FE29900E0826C /* Protocol.cpp */,
				BE58E12E217F295B00249358 /* Protocol.hpp */,
@@ -2276,6 +2284,8 @@
				BE8D77312136762500AC58EA /* DocumentViewController.mm */,
				BEFB1EDF21C29CC70081D757 /* L10n.h */,
				BEFB1EE021C29CC70081D757 /* L10n.mm */,
				BEDCC8972456FFAC00FB02BD /* SceneDelegate.h */,
				BEDCC8982456FFAC00FB02BD /* SceneDelegate.m */,
				BE80E45C21B6CEF100859C97 /* TemplateSectionHeaderView.h */,
				BE80E45D21B6CEF200859C97 /* TemplateSectionHeaderView.m */,
				BE80E45621B68F5000859C97 /* TemplateCollectionViewController.h */,
@@ -3032,8 +3042,10 @@
				BE5EB5C3213FE29900E0826C /* Session.cpp in Sources */,
				BE5EB5D22140039100E0826C /* LOOLWSD.cpp in Sources */,
				BEFB1EE121C29CC70081D757 /* L10n.mm in Sources */,
				BEDCC8992456FFAD00FB02BD /* SceneDelegate.m in Sources */,
				BE5EB5C6213FE29900E0826C /* SigUtil.cpp in Sources */,
				BE5EB5C1213FE29900E0826C /* Log.cpp in Sources */,
				BEDCC84E2452F82800FB02BD /* MobileApp.cpp in Sources */,
				BE5EB5DA2140363100E0826C /* ios.mm in Sources */,
				BE5EB5D421400DC100E0826C /* DocumentBroker.cpp in Sources */,
				BEA28377214FFD8C00848631 /* Unit.cpp in Sources */,
diff --git a/ios/Mobile/AppDelegate.mm b/ios/Mobile/AppDelegate.mm
index b761588..9e5f192 100644
--- a/ios/Mobile/AppDelegate.mm
+++ b/ios/Mobile/AppDelegate.mm
@@ -30,8 +30,6 @@
#import "LOOLWSD.hpp"
#import "Util.hpp"

static LOOLWSD *loolwsd = nullptr;

NSString *app_locale;

static void download(NSURL *source, NSURL *destination) {
@@ -206,6 +204,9 @@

    comphelper::LibreOfficeKit::setLanguageTag(LanguageTag(OUString::fromUtf8(OString([app_locale UTF8String])), true));

    // This fires off a thread running the LOKit runLoop()
    runKitLoopInAThread();

    // Look for the setting indicating the URL for a file containing a list of URLs for template
    // documents to download. If set, start a task to download it, and then to download the listed
    // templates.
@@ -247,26 +248,30 @@

    fakeSocketSetLoggingCallback([](const std::string& line)
                                 {
                                     LOG_TRC_NOFILE(line);
                                     LOG_INF(line);
                                 });

    dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                   ^{
                       assert(loolwsd == nullptr);
                       char *argv[2];
                       argv[0] = strdup([[NSBundle mainBundle].executablePath UTF8String]);
                       argv[1] = nullptr;
                       Util::setThreadName("app");
                       while (true) {
                           loolwsd = new LOOLWSD();
                           loolwsd->run(1, argv);
                           delete loolwsd;
                           LOG_TRC("One run of LOOLWSD completed");
                       }
                       auto loolwsd = new LOOLWSD();
                       loolwsd->run(1, argv);

                       // Should never return
                       assert(false);
                       NSLog(@"lolwsd->run() unexpectedly returned");
                       std::abort();
                   });
    return YES;
}

- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)) {
    return [UISceneConfiguration configurationWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

- (void)applicationWillResignActive:(UIApplication *)application {
}

@@ -285,17 +290,14 @@
    std::_Exit(1);
}

// This method is called when you use the "Share > Open in Collabora Office" functionality in the
// Files app. Possibly also in other use cases.
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)inputURL options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
    // Ensure the URL is a file URL
    if (!inputURL.isFileURL) {
        return NO;
    }

    // If we already have a document open, close it
    if (lok_document != nullptr && [DocumentViewController singleton] != nil) {
        [[DocumentViewController singleton] bye];
    }

    // Reveal / import the document at the URL
    DocumentBrowserViewController *documentBrowserViewController = (DocumentBrowserViewController *)self.window.rootViewController;
    [documentBrowserViewController revealDocumentAtURL:inputURL importIfNeeded:YES completion:^(NSURL * _Nullable revealedDocumentURL, NSError * _Nullable error) {
diff --git a/ios/Mobile/CODocument.h b/ios/Mobile/CODocument.h
index 79f772a..7aa262c 100644
--- a/ios/Mobile/CODocument.h
+++ b/ios/Mobile/CODocument.h
@@ -19,6 +19,7 @@
@public
    int fakeClientFd;
    NSURL *copyFileURL;
    unsigned appDocId;
}

@property (weak) DocumentViewController *viewController;
diff --git a/ios/Mobile/CODocument.mm b/ios/Mobile/CODocument.mm
index 7eaa9c9..c12bd2e 100644
--- a/ios/Mobile/CODocument.mm
+++ b/ios/Mobile/CODocument.mm
@@ -32,6 +32,7 @@
#import "KitHelper.hpp"
#import "Log.hpp"
#import "LOOLWSD.hpp"
#import "MobileApp.hpp"
#import "Protocol.hpp"

@implementation CODocument
@@ -40,6 +41,12 @@
    return [NSData dataWithContentsOfFile:[copyFileURL path] options:0 error:errorPtr];
}

// We keep a running count of opening documents here. This is not necessarily in sync with the
// DocBrokerId in DocumentBroker due to potential parallelism when opening multiple documents in
// quick succession.

static std::atomic<unsigned> appDocIdCounter(1);

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)errorPtr {

    // If this method is called a second time on the same CODocument object, just ignore it. This
@@ -59,10 +66,13 @@

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"loleaflet" withExtension:@"html"];
    NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
    appDocId = appDocIdCounter++;
    allocateDocumentDataForMobileAppDocId(appDocId).coDocument = self;
    components.queryItems = @[ [NSURLQueryItem queryItemWithName:@"file_path" value:[copyFileURL absoluteString]],
                               [NSURLQueryItem queryItemWithName:@"closebutton" value:@"1"],
                               [NSURLQueryItem queryItemWithName:@"permission" value:@"edit"],
                               [NSURLQueryItem queryItemWithName:@"lang" value:app_locale]
                               [NSURLQueryItem queryItemWithName:@"lang" value:app_locale],
                               [NSURLQueryItem queryItemWithName:@"appdocid" value:[NSString stringWithFormat:@"%u", appDocId]],
                             ];

    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:components.URL];
diff --git a/ios/Mobile/DocumentViewController.h b/ios/Mobile/DocumentViewController.h
index 2972559..cb845b3 100644
--- a/ios/Mobile/DocumentViewController.h
+++ b/ios/Mobile/DocumentViewController.h
@@ -20,7 +20,6 @@
@property (strong) NSURL *slideshowURL;

- (void)bye;
+ (DocumentViewController*)singleton;

@end

diff --git a/ios/Mobile/DocumentViewController.mm b/ios/Mobile/DocumentViewController.mm
index c94161c..3f8744f 100644
--- a/ios/Mobile/DocumentViewController.mm
+++ b/ios/Mobile/DocumentViewController.mm
@@ -22,13 +22,12 @@
#import "FakeSocket.hpp"
#import "LOOLWSD.hpp"
#import "Log.hpp"
#import "MobileApp.hpp"
#import "SigUtil.hpp"
#import "Util.hpp"

#import "DocumentViewController.h"

static DocumentViewController* theSingleton = nil;

@interface DocumentViewController() <WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, UIScrollViewDelegate, UIDocumentPickerDelegate> {
    int closeNotificationPipeForForwardingThread[2];
    NSURL *downloadAsTmpURL;
@@ -79,8 +78,6 @@
- (void)viewDidLoad {
    [super viewDidLoad];

    theSingleton = self;

    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *userContentController = [[WKUserContentController alloc] init];

@@ -317,10 +314,6 @@
                                           // is saved by closing it.
                                           fakeSocketClose(self->closeNotificationPipeForForwardingThread[1]);

                                           // Flag to make the inter-thread plumbing in the Online
                                           // bits go away quicker.
                                           MobileTerminationFlag = true;

                                           // Close our end of the fake socket connection to the
                                           // ClientSession thread, so that it terminates
                                           fakeSocketClose(self.document->fakeClientFd);
@@ -348,13 +341,14 @@
                               assert(false);
                           });

            // First we simply send it the URL. This corresponds to the GET request with Upgrade to
            // WebSocket.
            // First we simply send the Online C++ parts the URL and the appDocId. This corresponds
            // to the GET request with Upgrade to WebSocket.
            std::string url([[self.document->copyFileURL absoluteString] UTF8String]);
            p.fd = self.document->fakeClientFd;
            p.events = POLLOUT;
            fakeSocketPoll(&p, 1, -1);
            fakeSocketWrite(self.document->fakeClientFd, url.c_str(), url.size());
            std::string message(url + " " + std::to_string(self.document->appDocId));
            fakeSocketWrite(self.document->fakeClientFd, message.c_str(), message.size());

            return;
        } else if ([message.body isEqualToString:@"BYE"]) {
@@ -369,7 +363,7 @@
            self.slideshowFile = Util::createRandomTmpDir() + "/slideshow.svg";
            self.slideshowURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:self.slideshowFile.c_str()] isDirectory:NO];

            lok_document->saveAs([[self.slideshowURL absoluteString] UTF8String], "svg", nullptr);
            getDocumentDataForMobileAppDocId(self.document->appDocId).loKitDocument->saveAs([[self.slideshowURL absoluteString] UTF8String], "svg", nullptr);

            // Add a new full-screen WebView displaying the slideshow.

@@ -423,7 +417,7 @@

            std::string printFile = Util::createRandomTmpDir() + "/print.pdf";
            NSURL *printURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:printFile.c_str()] isDirectory:NO];
            lok_document->saveAs([[printURL absoluteString] UTF8String], "pdf", nullptr);
            getDocumentDataForMobileAppDocId(self.document->appDocId).loKitDocument->saveAs([[printURL absoluteString] UTF8String], "pdf", nullptr);

            UIPrintInteractionController *pic = [UIPrintInteractionController sharedPrintController];
            UIPrintInfo *printInfo = [UIPrintInfo printInfo];
@@ -476,7 +470,7 @@

                std::remove([[downloadAsTmpURL path] UTF8String]);

                lok_document->saveAs([[downloadAsTmpURL absoluteString] UTF8String], [format UTF8String], nullptr);
                getDocumentDataForMobileAppDocId(self.document->appDocId).loKitDocument->saveAs([[downloadAsTmpURL absoluteString] UTF8String], [format UTF8String], nullptr);

                // Then verify that it indeed was saved, and then use an
                // UIDocumentPickerViewController to ask the user where to store the exported
@@ -526,31 +520,17 @@
    // Close one end of the socket pair, that will wake up the forwarding thread above
    fakeSocketClose(closeNotificationPipeForForwardingThread[0]);

    // We can't wait for the LOOLWSD::lokit_main_mutex directly here because this is called on the
    // main queue and the main queue must be ready to execute code dispatched by the system APIs
    // used to do document saving.
    dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
    // deallocateDocumentDataForMobileAppDocId(self.document->appDocId);

    [[NSFileManager defaultManager] removeItemAtURL:self.document->copyFileURL error:nil];

    // The dismissViewControllerAnimated must be done on the main queue.
    dispatch_async(dispatch_get_main_queue(),
                   ^{
                       // Wait for lokit_main thread to exit
                       std::lock_guard<std::mutex> lock(LOOLWSD::lokit_main_mutex);

                       theSingleton = nil;

                       [[NSFileManager defaultManager] removeItemAtURL:self.document->copyFileURL error:nil];

                       // And only then let the document browsing view show up again. The
                       // dismissViewControllerAnimated must be done on the main queue.
                       dispatch_async(dispatch_get_main_queue(),
                                      ^{
                                          [self dismissDocumentViewController];
                                      });
                       [self dismissDocumentViewController];
                   });
}

+ (DocumentViewController*)singleton {
    return theSingleton;
}

@end

// vim:set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/ios/Mobile/Info.plist.in b/ios/Mobile/Info.plist.in
index 54d68f3..eb4b08e 100644
--- a/ios/Mobile/Info.plist.in
+++ b/ios/Mobile/Info.plist.in
@@ -421,6 +421,25 @@
		<string>share/fonts/truetype/opens___.ttf</string>
		@IOSAPP_FONTS@
	</array>
	<key>UIApplicationSceneManifest</key>
	<dict>
		<key>UIApplicationSupportsMultipleScenes</key>
		<true/>
		<key>UISceneConfigurations</key>
		<dict>
			<key>UIWindowSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneDelegateClassName</key>
					<string>SceneDelegate</string>
					<key>UISceneConfigurationName</key>
					<string>Default Configuration</string>
					<key>UISceneStoryboardFile</key>
					<string>Main</string>
				</dict>
			</array>
		</dict>
	</dict>
	<key>UIFileSharingEnabled</key>
	<true/>
	<key>UILaunchStoryboardName</key>
diff --git a/ios/Mobile/SceneDelegate.h b/ios/Mobile/SceneDelegate.h
new file mode 100644
index 0000000..d0c8f4d
--- /dev/null
+++ b/ios/Mobile/SceneDelegate.h
@@ -0,0 +1,18 @@
// -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*-
//
// This file is part of the LibreOffice project.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#import <UIKit/UIKit.h>

@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>

@property (strong, nonatomic) UIWindow * window;

@end

// vim:set shiftwidth=4 softtabstop=4 expandtab:

diff --git a/ios/Mobile/SceneDelegate.m b/ios/Mobile/SceneDelegate.m
new file mode 100644
index 0000000..b403d7d
--- /dev/null
+++ b/ios/Mobile/SceneDelegate.m
@@ -0,0 +1,20 @@
// -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*-
//
// This file is part of the LibreOffice project.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#import <UIKit/UIKit.h>

#import "SceneDelegate.h"

@implementation SceneDelegate

// Nothing needed so far, the window property in the .h file is enough.

@end

// vim:set shiftwidth=4 softtabstop=4 expandtab:

diff --git a/ios/config.h.in b/ios/config.h.in
index 18ec261..05934c2 100644
--- a/ios/config.h.in
+++ b/ios/config.h.in
@@ -89,10 +89,10 @@
#undef LT_OBJDIR

/* Limit the maximum number of open connections */
#define MAX_CONNECTIONS 3
#define MAX_CONNECTIONS 100

/* Limit the maximum number of open documents */
#define MAX_DOCUMENTS 1
#define MAX_DOCUMENTS 100

/* Define to 1 if this is a mobileapp (eg. Android) build. */
#define MOBILEAPP 1
diff --git a/ios/ios.h b/ios/ios.h
index a4283ce..198b9e9 100644
--- a/ios/ios.h
+++ b/ios/ios.h
@@ -7,12 +7,9 @@
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <mutex>

#include <LibreOfficeKit/LibreOfficeKit.hxx>

extern int loolwsd_server_socket_fd;
extern lok::Document *lok_document;

extern LibreOfficeKit *lo_kit;

diff --git a/ios/ios.mm b/ios/ios.mm
index 97c49c3..b5c6ac6 100644
--- a/ios/ios.mm
+++ b/ios/ios.mm
@@ -6,8 +6,6 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <cstring>

#include "ios.h"

#import <Foundation/Foundation.h>
@@ -18,7 +16,7 @@
}

int loolwsd_server_socket_fd = -1;
lok::Document *lok_document = nullptr;

LibreOfficeKit *lo_kit;

// vim:set shiftwidth=4 softtabstop=4 expandtab:
diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index ca9701e..cc8f741 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -10,6 +10,7 @@
#include <config.h>

#include "ChildSession.hpp"
#include "MobileApp.hpp"

#include <climits>
#include <fstream>
@@ -32,10 +33,6 @@
#include <Poco/Net/AcceptCertificateHandler.h>
#endif

#ifdef IOS
#import "DocumentViewController.h"
#endif

#include <common/FileUtil.hpp>
#include <common/JsonUtil.hpp>
#include <common/Authorization.hpp>
@@ -2486,11 +2483,10 @@

            if (!commandName.isEmpty() && commandName.toString() == ".uno:Save" && !success.isEmpty() && success.toString() == "true")
            {
                CODocument *document = [[DocumentViewController singleton] document];

                CODocument *document = getDocumentDataForMobileAppDocId(_docManager->getMobileAppDocId()).coDocument;
                [document saveToURL:[document fileURL]
                 forSaveOperation:UIDocumentSaveForOverwriting
                 completionHandler:^(BOOL success) {
                   forSaveOperation:UIDocumentSaveForOverwriting
                  completionHandler:^(BOOL success) {
                        LOG_TRC("ChildSession::loKitCallback() save completion handler gets " << (success?"YES":"NO"));
                    }];
            }
diff --git a/kit/ChildSession.hpp b/kit/ChildSession.hpp
index 4c4dbb2..48edf98 100644
--- a/kit/ChildSession.hpp
+++ b/kit/ChildSession.hpp
@@ -71,6 +71,8 @@
    virtual bool sendFrame(const char* buffer, int length, WSOpCode opCode = WSOpCode::Text) = 0;

    virtual void alertAllUsers(const std::string& cmd, const std::string& kind) = 0;

    virtual unsigned getMobileAppDocId() = 0;
};

struct RecordedEvent
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index 743251f..269120e 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -52,6 +52,7 @@

#include "ChildSession.hpp"
#include <Common.hpp>
#include <MobileApp.hpp>
#include <FileUtil.hpp>
#include "KitHelper.hpp"
#include "Kit.hpp"
@@ -768,7 +769,8 @@
             const std::string& docId,
             const std::string& url,
             std::shared_ptr<TileQueue> tileQueue,
             const std::shared_ptr<WebSocketHandler>& websocketHandler)
             const std::shared_ptr<WebSocketHandler>& websocketHandler,
             unsigned mobileAppDocId)
      : _loKit(loKit),
        _jailId(jailId),
        _docKey(docKey),
@@ -784,7 +786,8 @@
        _stop(false),
        _isLoading(0),
        _editorId(-1),
        _editorChangeWarning(false)
        _editorChangeWarning(false),
        _mobileAppDocId(mobileAppDocId)
    {
        LOG_INF("Document ctor for [" << _docKey <<
                "] url [" << anonymizeUrl(_url) << "] on child [" << _jailId <<
@@ -809,6 +812,11 @@
        {
            session.second->resetDocManager();
        }

#ifdef IOS
        deallocateDocumentDataForMobileAppDocId(_mobileAppDocId);
#endif

    }

    const std::string& getUrl() const { return _url; }
@@ -1247,6 +1255,11 @@
        alertAllUsers("errortoall: cmd=" + cmd + " kind=" + kind);
    }

    unsigned getMobileAppDocId() override
    {
        return _mobileAppDocId;
    }

    static void GlobalCallback(const int type, const char* p, void* data)
    {
        if (SigUtil::getTerminationFlag())
@@ -1465,9 +1478,6 @@
#endif
            LOG_INF("Document [" << anonymizeUrl(_url) << "] has no more views, but has " <<
                    _sessions.size() << " sessions still. Destroying the document.");
#ifdef IOS
            lok_document = nullptr;
#endif
#ifdef __ANDROID__
            _loKitDocumentForAndroidOnly.reset();
#endif
@@ -1716,9 +1726,7 @@
            const double totalTime = elapsed/1000.;
            LOG_DBG("Returned lokit::documentLoad(" << FileUtil::anonymizeUrl(pURL) << ") in " << totalTime << "ms.");
#ifdef IOS
            // The iOS app (and the Android one) has max one document open at a time, so we can keep
            // a pointer to it in a global.
            lok_document = _loKitDocument.get();
            getDocumentDataForMobileAppDocId(_mobileAppDocId).loKitDocument = _loKitDocument.get();
#endif
            if (!_loKitDocument || !_loKitDocument->get())
            {
@@ -2135,6 +2143,8 @@
#ifdef __ANDROID__
    friend std::shared_ptr<lok::Document> getLOKDocumentForAndroidOnly();
#endif

    const unsigned _mobileAppDocId;
};

#ifdef __ANDROID__
@@ -2153,10 +2163,42 @@
    std::chrono::steady_clock::time_point _pollEnd;
    std::shared_ptr<Document> _document;

public:
    KitSocketPoll() :
        SocketPoll("kit")
    {
#ifdef IOS
        terminationFlag = false;
#endif
    }

public:
    ~KitSocketPoll()
    {
#ifdef IOS
        std::unique_lock<std::mutex> lock(KSPollsMutex);
        std::shared_ptr<KitSocketPoll> p;
        auto i = KSPolls.begin();
        for (; i != KSPolls.end(); ++i)
        {
            p = i->lock();
            if (p && p.get() == this)
                break;
        }
        assert(i != KSPolls.end());
        KSPolls.erase(i);
#endif
    }

    static std::shared_ptr<KitSocketPoll> create()
    {
        KitSocketPoll *p = new KitSocketPoll();
        auto result = std::shared_ptr<KitSocketPoll>(p);
#ifdef IOS
        std::unique_lock<std::mutex> lock(KSPollsMutex);
        KSPolls.push_back(result);
        // KSPollsCV.notify_one();
#endif
        return result;
    }

    // process pending message-queue events.
@@ -2229,8 +2271,26 @@
    {
        _document = std::move(document);
    }

#ifdef IOS
    static std::mutex KSPollsMutex;
    // static std::condition_variable KSPollsCV;
    static std::vector<std::weak_ptr<KitSocketPoll>> KSPolls;

    std::mutex terminationMutex;
    std::condition_variable terminationCV;
    bool terminationFlag;
#endif
};

#ifdef IOS

std::mutex KitSocketPoll::KSPollsMutex;
// std::condition_variable KitSocketPoll::KSPollsCV;
std::vector<std::weak_ptr<KitSocketPoll>> KitSocketPoll::KSPolls;

#endif

class KitWebSocketHandler final : public WebSocketHandler
{
    std::shared_ptr<TileQueue> _queue;
@@ -2238,16 +2298,18 @@
    std::shared_ptr<lok::Office> _loKit;
    std::string _jailId;
    std::shared_ptr<Document> _document;
    KitSocketPoll &_ksPoll;
    std::shared_ptr<KitSocketPoll> _ksPoll;
    const unsigned _mobileAppDocId;

public:
    KitWebSocketHandler(const std::string& socketName, const std::shared_ptr<lok::Office>& loKit, const std::string& jailId, KitSocketPoll& ksPoll) :
    KitWebSocketHandler(const std::string& socketName, const std::shared_ptr<lok::Office>& loKit, const std::string& jailId, std::shared_ptr<KitSocketPoll> ksPoll, unsigned mobileAppDocId) :
        WebSocketHandler(/* isClient = */ true, /* isMasking */ false),
        _queue(std::make_shared<TileQueue>()),
        _socketName(socketName),
        _loKit(loKit),
        _jailId(jailId),
        _ksPoll(ksPoll)
        _ksPoll(ksPoll),
        _mobileAppDocId(mobileAppDocId)
    {
    }

@@ -2296,14 +2358,16 @@
            std::string url;
            URI::decode(docKey, url);
            LOG_INF("New session [" << sessionId << "] request on url [" << url << "].");
#ifndef IOS
            Util::setThreadName("kit_" + docId);

#endif
            if (!_document)
            {
                _document = std::make_shared<Document>(
                    _loKit, _jailId, docKey, docId, url, _queue,
                    std::static_pointer_cast<WebSocketHandler>(shared_from_this()));
                _ksPoll.setDocument(_document);
                    std::static_pointer_cast<WebSocketHandler>(shared_from_this()),
                    _mobileAppDocId);
                _ksPoll->setDocument(_document);
            }

            // Validate and create session.
@@ -2320,8 +2384,15 @@
            Log::shutdown();
            std::_Exit(EX_SOFTWARE);
#else
#ifdef IOS
            LOG_INF("Setting our KitSocketPoll's termination flag due to 'exit' command.");
            std::unique_lock<std::mutex> lock(_ksPoll->terminationMutex);
            _ksPoll->terminationFlag = true;
            _ksPoll->terminationCV.notify_all();
#else
            LOG_INF("Setting TerminationFlag due to 'exit' command.");
            SigUtil::setTerminationFlag();
#endif
            _document.reset();
#endif
        }
@@ -2360,6 +2431,11 @@
        LOG_WRN("Kit connection lost without exit arriving from wsd. Setting TerminationFlag");
        SigUtil::setTerminationFlag();
#endif
#ifdef IOS
        std::unique_lock<std::mutex> lock(_ksPoll->terminationMutex);
        _ksPoll->terminationFlag = true;
        _ksPoll->terminationCV.notify_all();
#endif
    }
};

@@ -2371,19 +2447,62 @@
/// Called by LOK main-loop the central location for data processing.
int pollCallback(void* pData, int timeoutUs)
{
#ifndef IOS
    if (!pData)
        return 0;
    else
        return reinterpret_cast<KitSocketPoll*>(pData)->kitPoll(timeoutUs);
#else
    std::unique_lock<std::mutex> lock(KitSocketPoll::KSPollsMutex);
    if (KitSocketPoll::KSPolls.size() == 0)
    {
        // KitSocketPoll::KSPollsCV.wait(lock);
        lock.unlock();
        std::this_thread::sleep_for(std::chrono::microseconds(timeoutUs));
    }
    else
    {
        std::vector<std::shared_ptr<KitSocketPoll>> v;
        for (const auto &i : KitSocketPoll::KSPolls)
        {
            auto p = i.lock();
            if (p)
                v.push_back(p);
        }
        lock.unlock();
        for (const auto &p : v)
            p->kitPoll(timeoutUs);
    }

    // We never want to exit the main loop
    return 0;
#endif
}

/// Called by LOK main-loop
void wakeCallback(void* pData)
{
#ifndef IOS
    if (!pData)
        return;
    else
        return reinterpret_cast<KitSocketPoll*>(pData)->wakeup();
#else
    std::unique_lock<std::mutex> lock(KitSocketPoll::KSPollsMutex);
    if (KitSocketPoll::KSPolls.size() == 0)
        return;

    std::vector<std::shared_ptr<KitSocketPoll>> v;
    for (const auto &i : KitSocketPoll::KSPolls)
    {
        auto p = i.lock();
        if (p)
            v.push_back(p);
    }
    lock.unlock();
    for (const auto &p : v)
        p->wakeup();
#endif
}

void setupKitEnvironment()
@@ -2435,10 +2554,9 @@
                bool queryVersion,
                bool displayVersion,
#else
                const std::string& documentUri,
                int docBrokerSocket,
#endif
                size_t spareKitId
                size_t numericIdentifier
                )
{
#if !MOBILEAPP
@@ -2448,7 +2566,7 @@
    SigUtil::setTerminationSignals();
#endif

    Util::setThreadName("kit_spare_" + Util::encodeId(spareKitId, 3));
    Util::setThreadName("kit_spare_" + Util::encodeId(numericIdentifier, 3));

    // Reinitialize logging when forked.
    const bool logToFile = std::getenv("LOOL_LOGFILE");
@@ -2497,8 +2615,6 @@
    std::shared_ptr<lok::Office> loKit;
    Path jailPath;
    ChildSession::NoCapsForKit = noCapabilities;
#else
    AnonymizeUserData = false;
#endif // MOBILEAPP

    try
@@ -2699,8 +2815,11 @@

#else // MOBILEAPP

        // was not done by the preload
#ifndef IOS
        // Was not done by the preload.
        // For iOS we call it in -[AppDelegate application: didFinishLaunchingWithOptions:]
        setupKitEnvironment();
#endif

#if defined(__linux) && !defined(__ANDROID__)
        Poco::URI userInstallationURI("file", LO_PATH);
@@ -2727,16 +2846,16 @@

#endif // MOBILEAPP

        KitSocketPoll mainKit;
        mainKit.runOnClientThread(); // We will do the polling on this thread.
        auto mainKit = KitSocketPoll::create();
        mainKit->runOnClientThread(); // We will do the polling on this thread.

        std::shared_ptr<KitWebSocketHandler> websocketHandler =
            std::make_shared<KitWebSocketHandler>("child_ws", loKit, jailId, mainKit);
            std::make_shared<KitWebSocketHandler>("child_ws", loKit, jailId, mainKit, numericIdentifier);

#if !MOBILEAPP
        mainKit.insertNewUnixSocket(MasterLocation, pathAndQuery, websocketHandler, ProcSMapsFile);
        mainKit->insertNewUnixSocket(MasterLocation, pathAndQuery, websocketHandler, ProcSMapsFile);
#else
        mainKit.insertNewFakeSocket(docBrokerSocket, websocketHandler);
        mainKit->insertNewFakeSocket(docBrokerSocket, websocketHandler);
#endif

        LOG_INF("New kit client websocket inserted.");
@@ -2749,6 +2868,7 @@
        }
#endif

#ifndef IOS
        if (!LIBREOFFICEKIT_HAS(kit, runLoop))
        {
            LOG_ERR("Kit is missing Unipoll API");
@@ -2758,7 +2878,7 @@

        LOG_INF("Kit unipoll loop run");

        loKit->runLoop(pollCallback, wakeCallback, &mainKit);
        loKit->runLoop(pollCallback, wakeCallback, mainKit.get());

        LOG_INF("Kit unipoll loop run terminated.");

@@ -2772,6 +2892,11 @@

        // Let forkit handle the jail cleanup.
#endif

#else // IOS
        std::unique_lock<std::mutex> lock(mainKit->terminationMutex);
        mainKit->terminationCV.wait(lock,[&]{ return mainKit->terminationFlag; } );
#endif // !IOS
    }
    catch (const Exception& exc)
    {
@@ -2793,7 +2918,35 @@

#endif
}
#endif

#ifdef IOS

// In the iOS app we can have several documents open in the app process at the same time, thus
// several lokit_main() functions running at the same time. We want just one LO main loop, though,
// so we start it separately in its own thread.

void runKitLoopInAThread()
{
    std::thread([&]
                {
                    Util::setThreadName("lokit_runloop");

                    std::shared_ptr<lok::Office> loKit = std::make_shared<lok::Office>(lo_kit);
                    int dummy;
                    loKit->runLoop(pollCallback, wakeCallback, &dummy);

                    // Should never return
                    assert(false);

                    NSLog(@"loKit->runLoop() unexpectedly returned");

                    std::abort();
                }).detach();
}

#endif // IOS

#endif // !BUILDING_TESTS

std::string anonymizeUrl(const std::string& url)
{
diff --git a/kit/Kit.hpp b/kit/Kit.hpp
index 1e60fdd..a5c6332 100644
--- a/kit/Kit.hpp
+++ b/kit/Kit.hpp
@@ -37,12 +37,15 @@
                bool queryVersionInfo,
                bool displayVersion,
#else
                const std::string& documentUri,
                int docBrokerSocket,
#endif
                size_t spareKitId
                size_t numericIdentifier
                );

#ifdef IOS
void runKitLoopInAThread();
#endif

/// We need to get several env. vars right
void setupKitEnvironment();

diff --git a/net/FakeSocket.cpp b/net/FakeSocket.cpp
index e8fd6bb..a4ef761 100644
--- a/net/FakeSocket.cpp
+++ b/net/FakeSocket.cpp
@@ -257,6 +257,11 @@
                    retval = true;
                }
            }
            if (fds[pollfds[i].fd/2].shutdown[N])
            {
                    pollfds[i].revents |= POLLHUP;
                    retval = true;
            }
        }
    }
    return retval;
diff --git a/test/WhiteBoxTests.cpp b/test/WhiteBoxTests.cpp
index a2e6761..47023d2 100644
--- a/test/WhiteBoxTests.cpp
+++ b/test/WhiteBoxTests.cpp
@@ -653,6 +653,11 @@
    void alertAllUsers(const std::string& /*cmd*/, const std::string& /*kind*/) override
    {
    }

    unsigned getMobileAppDocId() override
    {
        return 0;
    }
};

void WhiteBoxTests::testEmptyCellCursor()
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index da71968..fe99ed2 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -153,14 +153,6 @@

    bool continuePolling() override
    {
#if MOBILEAPP
        if (MobileTerminationFlag)
        {
            LOG_TRC("Noticed MobileTerminationFlag.");
            MobileTerminationFlag = false;
            return false;
        }
#endif
        return TerminatingPoll::continuePolling();
    }

@@ -176,7 +168,8 @@
DocumentBroker::DocumentBroker(ChildType type,
                               const std::string& uri,
                               const Poco::URI& uriPublic,
                               const std::string& docKey) :
                               const std::string& docKey,
                               unsigned mobileAppDocId) :
    _limitLifeSeconds(0),
    _uriOrig(uri),
    _type(type),
@@ -200,11 +193,16 @@
    _lockCtx(new LockContext()),
    _tileVersion(0),
    _debugRenderedTileCount(0),
    _wopiLoadDuration(0)
    _wopiLoadDuration(0),
    _mobileAppDocId(mobileAppDocId)
{
    assert(!_docKey.empty());
    assert(!LOOLWSD::ChildRoot.empty());

#ifdef IOS
    assert(_mobileAppDocId > 0);
#endif

    LOG_INF("DocumentBroker [" << LOOLWSD::anonymizeUrl(_uriPublic.toString()) <<
            "] created with docKey [" << _docKey << ']');
}
@@ -242,7 +240,7 @@
    do
    {
        static const int timeoutMs = COMMAND_TIMEOUT_MS * 5;
        _childProcess = getNewChild_Blocks(getPublicUri().getPath());
        _childProcess = getNewChild_Blocks();
        if (_childProcess ||
            std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
                                                                  _threadStart).count() > timeoutMs)
@@ -253,7 +251,8 @@
    }
    while (!_stop && _poll->continuePolling() && !SigUtil::getTerminationFlag() && !SigUtil::getShutdownRequestFlag());
#else
    _childProcess = getNewChild_Blocks(getPublicUri().getPath());
    assert(_mobileAppDocId > 0);
    _childProcess = getNewChild_Blocks(_mobileAppDocId);
#endif

    if (!_childProcess)
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 42d5066..4d09dff 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -128,11 +128,11 @@
    /// Dummy document broker that is marked to destroy.
    DocumentBroker();

    /// Construct DocumentBroker with URI, docKey, and root path.
    DocumentBroker(ChildType type,
                   const std::string& uri,
                   const Poco::URI& uriPublic,
                   const std::string& docKey);
                   const std::string& docKey,
                   unsigned mobileAppDocId = 0);

    virtual ~DocumentBroker();

@@ -439,6 +439,9 @@

    /// Unique DocBroker ID for tracing and debugging.
    static std::atomic<unsigned> DocBrokerId;

    // Relevant only in the mobile apps
    const unsigned _mobileAppDocId;
};

#if !MOBILEAPP
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 9c713fb..8cdf997 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -119,6 +119,7 @@
#  include <Kit.hpp>
#endif
#include <Log.hpp>
#include <MobileApp.hpp>
#include <Protocol.hpp>
#include <Session.hpp>
#if ENABLE_SSL
@@ -486,17 +487,20 @@
}

#if MOBILEAPP
#ifndef IOS
std::mutex LOOLWSD::lokit_main_mutex;
#endif
#endif

std::shared_ptr<ChildProcess> getNewChild_Blocks(const std::string& uri)
std::shared_ptr<ChildProcess> getNewChild_Blocks(unsigned mobileAppDocId)
{
    std::unique_lock<std::mutex> lock(NewChildrenMutex);

    const auto startTime = std::chrono::steady_clock::now();

#if !MOBILEAPP
    (void)uri;
    (void) mobileAppDocId;

    LOG_DBG("getNewChild: Rebalancing children.");
    int numPreSpawn = LOOLWSD::NumPreSpawnedChildren;
    ++numPreSpawn; // Replace the one we'll dispatch just now.
@@ -519,12 +523,15 @@

    std::thread([&]
                {
                    std::lock_guard<std::mutex> lokit_main_lock(LOOLWSD::lokit_main_mutex);
#ifndef IOS
                    std::lock_guard<std::mutex> lock(LOOLWSD::lokit_main_mutex);
                    Util::setThreadName("lokit_main");

                    // Ugly to have that static global, otoh we know there is just one LOOLWSD
                    // object. (Even in real Online.)
                    lokit_main(uri, LOOLWSD::prisonerServerSocketFD, 1);
#else
                    Util::setThreadName("lokit_main_" + Util::encodeId(mobileAppDocId, 3));
#endif
                    // Ugly to have that static global LOOLWSD::prisonerServerSocketFD, Otoh we know
                    // there is just one LOOLWSD object. (Even in real Online.)
                    lokit_main(LOOLWSD::prisonerServerSocketFD, mobileAppDocId);
                }).detach();
#endif

@@ -1853,7 +1860,8 @@
                          const std::string& uri,
                          const std::string& docKey,
                          const std::string& id,
                          const Poco::URI& uriPublic)
                          const Poco::URI& uriPublic,
                          unsigned mobileAppDocId = 0)
{
    LOG_INF("Find or create DocBroker for docKey [" << docKey <<
            "] for session [" << id << "] on url [" << LOOLWSD::anonymizeUrl(uriPublic.toString()) << "].");
@@ -1925,7 +1933,7 @@

        // Set the one we just created.
        LOG_DBG("New DocumentBroker for docKey [" << docKey << "].");
        docBroker = std::make_shared<DocumentBroker>(type, uri, uriPublic, docKey);
        docBroker = std::make_shared<DocumentBroker>(type, uri, uriPublic, docKey, mobileAppDocId);
        DocBrokers.emplace(docKey, docBroker);
        LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey << "].");
    }
@@ -2453,12 +2461,26 @@
        socket->eraseFirstInputBytes(map);
#else
        Poco::Net::HTTPRequest request;
        // The 2nd parameter is the response to the HULLO message (which we
        // respond with the path of the document)

#ifdef IOS
        // The URL of the document is sent over the FakeSocket by the code in
        // -[DocumentViewController userContentController:didReceiveScriptMessage:] when it gets the
        // HULLO message from the JavaScript in global.js.

        // The "app document id", the numeric id of the document, from the appDocIdCounter in CODocument.mm.
        char *space = strchr(socket->getInBuffer().data(), ' ');
        assert(space != nullptr);
        unsigned appDocId = std::strtoul(space + 1, nullptr, 10);

        handleClientWsUpgrade(
            request, std::string(socket->getInBuffer().data(), space - socket->getInBuffer().data()),
            disposition, socket, appDocId);
#else
        handleClientWsUpgrade(
            request, RequestDetails(std::string(socket->getInBuffer().data(),
                                                socket->getInBuffer().size())),
            disposition, socket);
#endif
        socket->getInBuffer().clear();
#endif
    }
@@ -2946,7 +2968,6 @@

        throw BadRequestException("Invalid or unknown request.");
    }
#endif

    void handleClientProxyRequest(const Poco::Net::HTTPRequest& request,
                                  const RequestDetails &requestDetails,
@@ -3053,11 +3074,13 @@
            streamSocket->shutdown();
        }
    }
#endif

    void handleClientWsUpgrade(const Poco::Net::HTTPRequest& request,
                               const RequestDetails &requestDetails,
                               SocketDisposition& disposition,
                               const std::shared_ptr<StreamSocket>& socket)
                               const std::shared_ptr<StreamSocket>& socket,
                               unsigned mobileAppDocId = 0)
    {
        const std::string url = requestDetails.getDocumentURI();
        assert(socket && "Must have a valid socket");
@@ -3114,7 +3137,7 @@
            // Request a kit process for this doc.
            std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
                std::static_pointer_cast<ProtocolHandlerInterface>(ws),
                DocumentBroker::ChildType::Interactive, url, docKey, _id, uriPublic);
                DocumentBroker::ChildType::Interactive, url, docKey, _id, uriPublic, mobileAppDocId);
            if (docBroker)
            {
                std::shared_ptr<ClientSession> clientSession =
@@ -3510,11 +3533,13 @@

        void wakeupHook() override
        {
#if !MOBILEAPP
            if (SigUtil::getDumpGlobalState())
            {
                dump_state();
                SigUtil::resetDumpGlobalState();
            }
#endif
        }
    };
    /// This thread & poll accepts incoming connections.
@@ -3971,7 +3996,7 @@

int LOOLWSD::main(const std::vector<std::string>& /*args*/)
{
#if MOBILEAPP
#if MOBILEAPP && !defined IOS
    SigUtil::resetTerminationFlag();
#endif

@@ -3992,10 +4017,6 @@

    UnitWSD::get().returnValue(returnValue);

#if MOBILEAPP
    fakeSocketDumpState();
#endif

    LOG_INF("Process [loolwsd] finished.");
    return returnValue;
}
diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp
index 8d9653c..54d6396 100644
--- a/wsd/LOOLWSD.hpp
+++ b/wsd/LOOLWSD.hpp
@@ -34,7 +34,7 @@
class DocumentBroker;
class ClipboardCache;

std::shared_ptr<ChildProcess> getNewChild_Blocks(const std::string& uri);
std::shared_ptr<ChildProcess> getNewChild_Blocks(unsigned mobileAppDocId = 0);

// A WSProcess object in the WSD process represents a descendant process, either the direct child
// process FORKIT or a grandchild KIT process, with which the WSD process communicates through a
@@ -257,9 +257,11 @@
    static std::set<const Poco::Util::AbstractConfiguration*> PluginConfigurations;
    static std::chrono::time_point<std::chrono::system_clock> StartTime;
#if MOBILEAPP
#ifndef IOS
    /// This is used to be able to wait until the lokit main thread has finished (and it is safe to load a new document).
    static std::mutex lokit_main_mutex;
#endif
#endif

    /// For testing only [!]
    static int getClientPortNumber();