diff --git a/lib/Controller/OCSController.php b/lib/Controller/OCSController.php index ae562af9d7..46ed51e83c 100644 --- a/lib/Controller/OCSController.php +++ b/lib/Controller/OCSController.php @@ -14,6 +14,7 @@ use OCA\Richdocuments\DirectEditing\OfficeDirectEditor; use OCA\Richdocuments\Exceptions\ExpiredTokenException; use OCA\Richdocuments\Exceptions\UnknownTokenException; +use OCA\Richdocuments\Listener\RegisterDirectEditorListener; use OCA\Richdocuments\Service\FederationService; use OCA\Richdocuments\TemplateManager; use OCA\Richdocuments\TokenManager; @@ -86,21 +87,31 @@ public function createDirect($fileId) { throw new OCSBadRequestException('Cannot view folder'); } - // getRelativePath() can return null for nodes outside the user - // folder; Manager::open() requires a string, so fall back to the - // filename which is enough for the manager to resolve by fileId. - $path = $userFolder->getRelativePath($node->getPath()) ?? $node->getName(); - - // Register directly so the legacy endpoint keeps working for - // older mobile clients where RegisterDirectEditorListener gates - // out the editor from OCP\DirectEditing discovery. - $this->directEditingManager->registerDirectEditor($this->officeDirectEditor); - /** @psalm-suppress UndefinedInterfaceMethod IManager does not expose open() but the concrete Manager does, same pattern as files-app DirectEditingController */ - $token = $this->directEditingManager->open($path, Application::APPNAME, $node->getId()); + if ($this->isAndroidV34OrAbove()) { + // Android v34+ uses the server's OCP\DirectEditing API natively + // (via /ocs/v2.php/apps/files/directEditing/open) but may fall back + // to this legacy endpoint — serve the server-managed URL so the + // TextEditorWebView flow works end-to-end. + $path = $userFolder->getRelativePath($node->getPath()) ?? $node->getName(); + $this->directEditingManager->registerDirectEditor($this->officeDirectEditor); + /** @psalm-suppress UndefinedInterfaceMethod IManager does not expose open() but the concrete Manager does, same pattern as files-app DirectEditingController */ + $token = $this->directEditingManager->open($path, Application::APPNAME, $node->getId()); + return new DataResponse([ + 'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', [ + 'token' => $token, + ]), + ]); + } + // iOS (all versions) and Android < 34: use the legacy richdocuments direct + // token. These clients open the URL in RichDocumentsEditorWebView which + // injects window.RichDocumentsMobileInterface. The server's generic + // TextEditorWebView (used for Android v34+) injects a different interface + // and these older clients cannot drive it. + $direct = $this->directMapper->newDirect($this->userId, $node->getId()); return new DataResponse([ - 'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', [ - 'token' => $token, + 'url' => $this->urlGenerator->linkToRouteAbsolute('richdocuments.directView.show', [ + 'token' => $direct->getToken(), ]), ]); } catch (NotFoundException) { @@ -108,6 +119,14 @@ public function createDirect($fileId) { } } + private function isAndroidV34OrAbove(): bool { + $userAgent = $this->request->getHeader('User-Agent'); + if (preg_match(IRequest::USER_AGENT_CLIENT_ANDROID, $userAgent, $matches) !== 1) { + return false; + } + return (int)explode('.', $matches[1] ?? '0')[0] >= RegisterDirectEditorListener::MIN_MOBILE_CLIENT_VERSION; + } + /** * Generate a direct editing link for a file in a public share to open with the current user * diff --git a/lib/Listener/RegisterDirectEditorListener.php b/lib/Listener/RegisterDirectEditorListener.php index 396b4ba8e8..76688afa2d 100644 --- a/lib/Listener/RegisterDirectEditorListener.php +++ b/lib/Listener/RegisterDirectEditorListener.php @@ -24,7 +24,7 @@ final class RegisterDirectEditorListener implements IEventListener { * legacy /apps/richdocuments/api/v1/document endpoint and should not * see the editor in OCP\DirectEditing discovery responses. */ - private const MIN_MOBILE_CLIENT_VERSION = 34; + public const MIN_MOBILE_CLIENT_VERSION = 34; public function __construct( private OfficeDirectEditor $editor, diff --git a/src/helpers/mobile.js b/src/helpers/mobile.js index 129616c313..62b5e0100f 100644 --- a/src/helpers/mobile.js +++ b/src/helpers/mobile.js @@ -8,11 +8,13 @@ import Config from './../services/config.tsx' const isDirectEditing = () => Config.get('direct') const isMobileInterfaceAvailable = () => window.RichDocumentsMobileInterface + || window.DirectEditingMobileInterface || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface) const isMobileInterfaceOnIos = () => window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface const isMobileInterfaceOnAndroid = () => window.RichDocumentsMobileInterface + || window.DirectEditingMobileInterface const callMobileMessage = (messageName, attributes) => { console.debug('callMobileMessage', messageName, attributes) @@ -29,7 +31,7 @@ const callMobileMessage = (messageName, attributes) => { } catch (e) { attributesString = null } - // Forward to mobile handler + // Forward to RichDocuments-specific mobile handler (legacy richdocuments WebView) if (window.RichDocumentsMobileInterface && typeof window.RichDocumentsMobileInterface[messageName] === 'function') { if (attributesString === null || typeof attributesString === 'undefined') { window.RichDocumentsMobileInterface[messageName]() @@ -38,6 +40,15 @@ const callMobileMessage = (messageName, attributes) => { } } + // Forward to generic direct editing mobile handler (server's OCP\DirectEditing WebView, Android v34+) + if (window.DirectEditingMobileInterface && typeof window.DirectEditingMobileInterface[messageName] === 'function') { + if (attributesString === null || typeof attributesString === 'undefined') { + window.DirectEditingMobileInterface[messageName]() + } else { + window.DirectEditingMobileInterface[messageName](attributesString) + } + } + // iOS webkit fallback if (window.webkit && window.webkit.messageHandlers