Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,13 @@
package it.niedermann.owncloud.notes.edit

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.http.SslError
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ScrollView
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
Expand All @@ -39,6 +30,10 @@ import it.niedermann.owncloud.notes.branding.Branded
import it.niedermann.owncloud.notes.branding.BrandedSnackbar
import it.niedermann.owncloud.notes.branding.BrandingUtil
import it.niedermann.owncloud.notes.databinding.FragmentNoteDirectEditBinding
import it.niedermann.owncloud.notes.edit.directeditingwebview.DirectEditingMobileInterface
import it.niedermann.owncloud.notes.edit.directeditingwebview.DirectEditingWebViewClient
import it.niedermann.owncloud.notes.edit.directeditingwebview.DirectEditingWebViewProvider
import it.niedermann.owncloud.notes.edit.directeditingwebview.openDirectEditing
import it.niedermann.owncloud.notes.persistence.ApiProvider
import it.niedermann.owncloud.notes.persistence.DirectEditingRepository
import it.niedermann.owncloud.notes.persistence.entity.Note
Expand All @@ -57,6 +52,13 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {

private val disposables: DisposableSet = DisposableSet()
private var switchToEditPending = false
private var webView: WebView? = null

private val mobileInterfaceListener = object : DirectEditingMobileInterface.Listener {
override fun onClose() = close()
override fun onShare() = share()
override fun onLoaded() = this@NoteDirectEditFragment.onLoaded()
}

val account: SingleSignOnAccount by lazy {
SingleAccountHelper.getCurrentSingleSignOnAccount(
Expand Down Expand Up @@ -85,10 +87,14 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
savedInstanceState: Bundle?,
): View? {
Log.d(TAG, "onCreateView() called")
_binding = FragmentNoteDirectEditBinding.inflate(inflater, container, false)
val viewBinding = FragmentNoteDirectEditBinding.inflate(inflater, container, false).also { _binding = it }
webView = DirectEditingWebViewProvider.attach(
viewBinding.noteWebviewContainer,
DirectEditingWebViewClient(runCatching { account.url }.getOrNull(), ::handleLoadError),
mobileInterfaceListener,
)
setupFab()
prepareWebView()
return binding?.root
return viewBinding.root
}

@SuppressLint("ClickableViewAccessibility") // touch listener only for UI purposes, no need to handle click
Expand All @@ -98,7 +104,7 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
ExtendedFabUtil.toggleExtendedOnLongClick(plainEditingFab)

// manually detect scroll as we can't get it from the webview (maybe with custom JS?)
noteWebview.setOnTouchListener { _, event ->
webView?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
scrollStart = event.y.toInt()
Expand All @@ -121,7 +127,7 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {

private fun switchToPlainEdit() {
switchToEditPending = true
binding?.noteWebview?.evaluateJavascript(JS_CLOSE) { result ->
webView?.evaluateJavascript(JS_CLOSE) { result ->
val resultWithoutQuotes = result.replace("\"", "")
if (resultWithoutQuotes != JS_RESULT_OK) {
Log.w(TAG, "Closing via JS failed: $resultWithoutQuotes")
Expand All @@ -134,7 +140,8 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
override fun onDestroyView() {
super.onDestroyView()
disposables.dispose()
binding?.noteWebview?.destroy()
binding?.let { DirectEditingWebViewProvider.detach(it.noteWebviewContainer) }
webView = null
_binding = null
}

Expand All @@ -143,7 +150,7 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
val timeoutDisposable = Single.just(Unit)
.delay(LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.map {
if (binding?.noteWebview?.isVisible == false) {
if (binding?.noteWebviewContainer?.isVisible == false) {
Log.w(TAG, "Editor not loaded after $LOAD_TIMEOUT_SECONDS seconds")
handleLoadError()
}
Expand Down Expand Up @@ -208,7 +215,7 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
if (BuildConfig.DEBUG) {
Log.d(TAG, "loadNoteInWebView: url = $url")
}
binding?.noteWebview?.loadUrl(url)
webView?.openDirectEditing(url)
}
}, { throwable ->
handleLoadError()
Expand Down Expand Up @@ -242,94 +249,6 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {

override fun shouldShowToolbar(): Boolean = false

@SuppressLint("SetJavaScriptEnabled")
private fun prepareWebView() {
binding?.noteWebview?.settings?.run {
// enable zoom
setSupportZoom(true)
builtInZoomControls = true
displayZoomControls = false

// Non-responsive webs are zoomed out when loaded
useWideViewPort = true
loadWithOverviewMode = true

// user agent
val userAgent =
getString(R.string.user_agent, getString(R.string.app_name), BuildConfig.VERSION_NAME)
userAgentString = userAgent

// no private data storing
savePassword = false
saveFormData = false

// disable local file access
allowFileAccess = false

// enable javascript
javaScriptEnabled = true
domStorageEnabled = true
}

if (BuildConfig.DEBUG) {
// caching disabled in debug mode
binding?.noteWebview?.settings?.cacheMode = WebSettings.LOAD_NO_CACHE
}

binding?.noteWebview?.addJavascriptInterface(
DirectEditingMobileInterface(this),
JS_INTERFACE_NAME,
)

binding?.noteWebview?.webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?,
) {
super.onReceivedError(view, request, error)
if (request?.isForMainFrame == true) {
handleLoadError()
}
}

@SuppressLint("WebViewClientOnReceivedSslError") // only for debug mode
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?,
) {
if (BuildConfig.DEBUG) {
handler?.proceed()
} else {
super.onReceivedSslError(view, handler, error)
}
}

override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
val url = request?.url ?: return false
val scheme = url.scheme?.lowercase()
val accountUrl = runCatching { account.url }.getOrNull()
val staysOnServer = (scheme == "http" || scheme == "https") &&
accountUrl != null && url.toString().startsWith(accountUrl)
if (!staysOnServer) {
try {
val intent = Intent(Intent.ACTION_VIEW, url).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No app available to open $url", e)
}
}
return !staysOnServer
}
}
}

/**
* Gets the current content of the EditText field in the UI.
*
Expand Down Expand Up @@ -358,23 +277,6 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
}
}

private class DirectEditingMobileInterface(val noteDirectEditFragment: NoteDirectEditFragment) {
@JavascriptInterface
fun close() {
noteDirectEditFragment.close()
}

@JavascriptInterface
fun share() {
noteDirectEditFragment.share()
}

@JavascriptInterface
fun loaded() {
noteDirectEditFragment.onLoaded()
}
}

private fun close() {
if (switchToEditPending) {
Log.d(TAG, "close: switching to plain edit")
Expand Down Expand Up @@ -422,7 +324,7 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
activity?.runOnUiThread {
binding?.run {
progress.isVisible = loading
noteWebview.isVisible = !loading
noteWebviewContainer.isVisible = !loading
plainEditingFab.isVisible = !loading
}
}
Expand All @@ -431,7 +333,6 @@ class NoteDirectEditFragment : BaseNoteFragment(), Branded {
companion object {
private const val TAG = "NoteDirectEditFragment"
private const val LOAD_TIMEOUT_SECONDS = 10L
private const val JS_INTERFACE_NAME = "DirectEditingMobileInterface"
private const val JS_RESULT_OK = "ok"

// language=js
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package it.niedermann.owncloud.notes.edit.directeditingwebview

import android.webkit.JavascriptInterface

class DirectEditingMobileInterface {
@Volatile
var listener: Listener? = null

@JavascriptInterface
fun close() {
listener?.onClose()
}

@JavascriptInterface
fun share() {
listener?.onShare()
}

@JavascriptInterface
fun loaded() {
listener?.onLoaded()
}

interface Listener {
fun onClose()
fun onShare()
fun onLoaded()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package it.niedermann.owncloud.notes.edit.directeditingwebview

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.net.http.SslError
import android.util.Log
import android.webkit.SslErrorHandler
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import it.niedermann.owncloud.notes.BuildConfig

class DirectEditingWebViewClient(
private val serverUrl: String?,
private val onMainFrameError: () -> Unit,
) : WebViewClient() {

override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
super.onReceivedError(view, request, error)
if (request?.isForMainFrame == true) {
onMainFrameError()
}
}

@SuppressLint("WebViewClientOnReceivedSslError")
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
if (BuildConfig.DEBUG) {
handler?.proceed()
} else {
super.onReceivedSslError(view, handler, error)
}
}

override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url ?: return false
if (staysOnServer(url)) return false
view?.let { openExternally(it, url) }
return true
}

private fun staysOnServer(url: Uri): Boolean {
val scheme = url.scheme?.lowercase()
return (scheme == "http" || scheme == "https") &&
serverUrl != null && url.toString().startsWith(serverUrl)
}

private fun openExternally(view: WebView, url: Uri) {
try {
val intent = Intent(Intent.ACTION_VIEW, url).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
view.context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No app available to open $url", e)
}
}

companion object {
private const val TAG = "DirectEditingWebViewClient"
}
}
Loading
Loading