From 28a8672443cc185229f3f17931eb63514aa59435 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 20 Oct 2026 11:36:00 +0200 Subject: [PATCH 1/4] refactor(web-view-util): use ProxyController Signed-off-by: alperozturk96 --- .../utils/extensions/ContextExtensions.kt | 37 +++---- .../authentication/AuthenticatorActivity.java | 9 +- .../android/ui/activity/EditorWebView.kt | 22 ++--- .../ui/activity/ExternalSiteWebView.kt | 14 ++- .../ui/fragment/OCFileListFragment.java | 4 +- .../com/owncloud/android/utils/ProxyUtil.kt | 52 ++++++++++ .../com/owncloud/android/utils/WebViewUtil.kt | 99 ------------------- 7 files changed, 93 insertions(+), 144 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt delete mode 100644 app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt index 53b3c35495b6..3dcd944567bb 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt @@ -19,15 +19,25 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.provider.Settings -import android.view.WindowInsets -import android.view.WindowManager +import android.util.Log import android.widget.Toast import androidx.core.net.toUri -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.webkit.WebViewCompat import com.owncloud.android.R import com.owncloud.android.datamodel.ReceiverFlag +private const val TAG = "ContextExtensions" + +@Suppress("TooGenericExceptionCaught") +fun Context.isWebViewAvailable(): Boolean { + return try { + WebViewCompat.getCurrentWebViewPackage(this) != null + } catch (e: Exception) { + Log.w(TAG, "WebView provider cannot be resolved: ${e.message}") + false + } +} + fun Context.hourPlural(hour: Int): String = resources.getQuantityString(R.plurals.hours, hour, hour) fun Context.minPlural(min: Int): String = resources.getQuantityString(R.plurals.minutes, min, min) @@ -40,25 +50,6 @@ fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: Inte registerReceiver(receiver, filter) } -fun Context.statusBarHeight(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val windowInsets = (getSystemService(Context.WINDOW_SERVICE) as WindowManager) - .currentWindowMetrics - .windowInsets - val insets = windowInsets.getInsets(WindowInsets.Type.statusBars()) - insets.top -} else { - @Suppress("DEPRECATION") - val decorView = (getSystemService(Context.WINDOW_SERVICE) as WindowManager) - .defaultDisplay - .let { display -> - val decorView = android.view.View(this) - display.getRealMetrics(android.util.DisplayMetrics()) - decorView - } - val windowInsetsCompat = ViewCompat.getRootWindowInsets(decorView) - windowInsetsCompat?.getInsets(WindowInsetsCompat.Type.statusBars())?.top ?: 0 -} - fun Context.showToast(message: String) { Handler(Looper.getMainLooper()).post { Toast.makeText(this, message, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index d1d39232eb9d..0f155f13cf07 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -105,9 +105,10 @@ import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.WebViewUtil; +import com.owncloud.android.utils.ProxyUtil; import com.owncloud.android.utils.theme.ViewThemeUtils; +import java.lang.ref.WeakReference; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; @@ -949,7 +950,6 @@ protected void onNewIntent(Intent intent) { private void initSimpleSignupLogin() { viewThemeUtils.platform.colorCircularProgressBar(accountSetupWebviewBinding.loginWebviewProgressBar, ColorRole.ON_PRIMARY_CONTAINER); accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); - new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); accountSetupWebviewBinding.loginWebview.getSettings().setAllowFileAccess(false); accountSetupWebviewBinding.loginWebview.getSettings().setJavaScriptEnabled(true); @@ -962,9 +962,8 @@ private void initSimpleSignupLogin() { Map headers = new HashMap<>(); headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); - new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); - - accountSetupWebviewBinding.loginWebview.loadUrl(getString(R.string.provider_registration_server), headers); + new ProxyUtil(new WeakReference<>(this)).applyProxy(() -> + accountSetupWebviewBinding.loginWebview.loadUrl(getString(R.string.provider_registration_server), headers)); accountSetupWebviewBinding.loginFlowV2.loginFlowInfoV2.setVisibility(View.GONE); setClient(); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt index a5043cb401b5..daf0f2ab5b46 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User +import com.nextcloud.utils.AppConstants.ONE_SECOND import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getSmallThumbnail @@ -36,11 +37,9 @@ import com.owncloud.android.datamodel.SyncedFolderObserver import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.RichDocumentDownloader -import com.owncloud.android.utils.WebViewUtil import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds -import com.nextcloud.utils.AppConstants.ONE_SECOND abstract class EditorWebView : ExternalSiteWebView() { private lateinit var binding: RichdocumentsWebviewBinding @@ -83,17 +82,18 @@ abstract class EditorWebView : ExternalSiteWebView() { return } - lifecycleScope.launch { - WebViewUtil().setProxyKKPlus(webView) - delay(ONE_SECOND) + proxyUtil?.applyProxy { + lifecycleScope.launch { + delay(ONE_SECOND) - if (loadedUrl != webView.url) { - webView.loadUrl(loadedUrl) - } + if (loadedUrl != webView.url) { + webView.loadUrl(loadedUrl) + } - delay(LOADING_TIMEOUT) - if (!webView.isVisible) { - showLoadingTimeoutSnackbar() + delay(LOADING_TIMEOUT) + if (!webView.isVisible) { + showLoadingTimeoutSnackbar() + } } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt index 4ce4b705a7a6..ab9dd2dc2d43 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt @@ -23,13 +23,15 @@ import androidx.drawerlayout.widget.DrawerLayout import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.RawResourceReader import com.nextcloud.utils.SnackbarUtil +import com.nextcloud.utils.extensions.isWebViewAvailable import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.ExternalsiteWebviewBinding import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.NextcloudWebViewClient -import com.owncloud.android.utils.WebViewUtil +import com.owncloud.android.utils.ProxyUtil import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.lang.ref.WeakReference /** * This activity shows an URL as a web view @@ -40,6 +42,7 @@ open class ExternalSiteWebView : FileActivity() { private var showToolbar = true private var showSidebar = false protected var url: String? = null + protected var proxyUtil: ProxyUtil? = null private val isDebuggable: Boolean get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 @@ -47,13 +50,15 @@ open class ExternalSiteWebView : FileActivity() { final override fun onCreate(savedInstanceState: Bundle?) { Log_OC.v(TAG, "onCreate() start") - if (!WebViewUtil.available(this)) { + if (!this.isWebViewAvailable()) { super.onCreate(savedInstanceState) SnackbarUtil.show(this, R.string.webview_not_available) finish() return } + proxyUtil = ProxyUtil(WeakReference(this)) + bindView() readIntentExtras() window?.requestFeature(Window.FEATURE_PROGRESS) @@ -87,8 +92,9 @@ open class ExternalSiteWebView : FileActivity() { setupWebSettings(webView.settings) webView.webViewClient = createWebViewClient() - WebViewUtil().setProxyKKPlus(webView) - url?.let { webView.loadUrl(it) } + proxyUtil?.applyProxy { + url?.let { webView.loadUrl(it) } + } } private fun setupToolbarAndDrawer() { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index fb7c402ac904..e1fdf9a331a5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -63,6 +63,7 @@ import com.nextcloud.utils.e2ee.E2EEActionResolver; import com.nextcloud.utils.e2ee.E2EEDialogPresenter; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.extensions.ContextExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.FragmentExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; @@ -118,7 +119,6 @@ import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.WebViewUtil; import com.owncloud.android.utils.theme.ThemeUtils; import org.greenrobot.eventbus.EventBus; @@ -1262,7 +1262,7 @@ private void handlePendingDownloadFile(OCFile file) { return; } - boolean webViewAvailable = WebViewUtil.available(context); + boolean webViewAvailable = ContextExtensionsKt.isWebViewAvailable(context); if (!file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda && fda.canPreviewInAudioPlayer(file)) { setFabVisible(false); diff --git a/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt new file mode 100644 index 000000000000..22970ef8a430 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.utils + +import android.annotation.SuppressLint +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.webkit.ProxyConfig +import androidx.webkit.ProxyController +import androidx.webkit.WebViewFeature +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import java.lang.ref.WeakReference + +class ProxyUtil(private val context: WeakReference) { + companion object { + private const val TAG = "ProxyUtil" + } + + @SuppressLint("RequiresFeature") + fun applyProxy(onApplied: Runnable) { + val context = context.get() ?: return + val proxyHost = OwnCloudClientManagerFactory.getProxyHost() + val proxyPort = OwnCloudClientManagerFactory.getProxyPort() + + if (proxyHost.isNullOrEmpty() || proxyPort <= 0) { + onApplied.run() + return + } + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + Log_OC.w(TAG, "WebView does not support proxy override") + onApplied.run() + return + } + + val proxyConfig = ProxyConfig.Builder() + .addProxyRule("$proxyHost:$proxyPort") + .build() + + ProxyController.getInstance().setProxyOverride( + proxyConfig, + ContextCompat.getMainExecutor(context), + onApplied + ) + } +} diff --git a/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt b/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt deleted file mode 100644 index 5badaecd64ba..000000000000 --- a/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Alper Ozturk - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.net.Proxy -import android.text.TextUtils -import android.util.ArrayMap -import android.util.Log -import android.webkit.WebView -import androidx.webkit.WebViewCompat -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory -import java.io.PrintWriter -import java.io.StringWriter - -class WebViewUtil { - - /** - * From https://stackoverflow.com/a/18453384 - * - * @return - */ - @SuppressLint("PrivateApi", "DiscouragedPrivateApi") - @Suppress("TooGenericExceptionCaught", "NestedBlockDepth") - fun setProxyKKPlus(webView: WebView) { - val proxyHost = OwnCloudClientManagerFactory.getProxyHost() - val proxyPort = OwnCloudClientManagerFactory.getProxyPort() - - if (TextUtils.isEmpty(proxyHost) || proxyPort <= 0) { - return - } - - val applicationClassName = "android.app.Application" - Log.d(PROXY_TAG, "Setting proxy with >= 4.4 API.") - - val appContext = webView.context.applicationContext - - System.setProperty("http.proxyHost", proxyHost) - System.setProperty("http.proxyPort", proxyPort.toString()) - System.setProperty("https.proxyHost", proxyHost) - System.setProperty("https.proxyPort", proxyPort.toString()) - try { - val applicationClass = Class.forName(applicationClassName) - val loadedApkField = applicationClass.getField("mLoadedApk") - loadedApkField.isAccessible = true - val loadedApk = loadedApkField[appContext] - val loadedApkCls = Class.forName("android.app.LoadedApk") - val receiversField = loadedApkCls.getDeclaredField("mReceivers") - receiversField.isAccessible = true - val receivers = receiversField[loadedApk] as ArrayMap<*, *> - for (receiverMap in receivers.values) { - for (rec in (receiverMap as ArrayMap<*, *>).keys) { - val clazz: Class<*> = rec.javaClass - if (clazz.name.contains("ProxyChangeListener")) { - val onReceiveMethod = clazz.getDeclaredMethod( - "onReceive", - Context::class.java, - Intent::class.java - ) - val intent = Intent(Proxy.PROXY_CHANGE_ACTION) - onReceiveMethod.invoke(rec, appContext, intent) - } - } - } - Log.d(PROXY_TAG, "Setting proxy with >= 4.4 API successful!") - } catch (e: Exception) { - val sw = StringWriter() - e.printStackTrace(PrintWriter(sw)) - val exceptionAsString = sw.toString() - e.message?.let { Log.v(PROXY_TAG, it) } - Log.v(PROXY_TAG, exceptionAsString) - } - } - - companion object { - private const val PROXY_TAG = "PROXY" - private const val WEB_VIEW_TAG = "WEB_VIEW" - - @JvmStatic - @Suppress("TooGenericExceptionCaught") - fun available(context: Context?): Boolean { - val context = context ?: return false - - return try { - WebViewCompat.getCurrentWebViewPackage(context) != null - } catch (e: Exception) { - Log.w(WEB_VIEW_TAG, "WebView provider cannot be resolved: ${e.message}") - false - } - } - } -} From ceb1d38c48c407d0cc69069c0b5a76c809d64650 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 21 Oct 2026 11:49:21 +0200 Subject: [PATCH 2/4] Wait for proxy only if there's a proxy configured Signed-off-by: daniele-verducci --- .../java/com/owncloud/android/ui/activity/EditorWebView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt index daf0f2ab5b46..1c147822cf4e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt @@ -34,6 +34,7 @@ import com.owncloud.android.R import com.owncloud.android.databinding.RichdocumentsWebviewBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderObserver +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.RichDocumentDownloader @@ -84,7 +85,9 @@ abstract class EditorWebView : ExternalSiteWebView() { proxyUtil?.applyProxy { lifecycleScope.launch { - delay(ONE_SECOND) + if (OwnCloudClientManagerFactory.getProxyHost().isNotEmpty()) { + delay(ONE_SECOND) + } if (loadedUrl != webView.url) { webView.loadUrl(loadedUrl) From 760bd4d79da1f279187028b18eedfe16799ef651 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Oct 2026 08:25:03 +0200 Subject: [PATCH 3/4] fix kotlin spotless bug Signed-off-by: alperozturk96 --- .../nextcloud/utils/extensions/ContextExtensions.kt | 12 +++++------- .../java/com/owncloud/android/utils/ProxyUtil.kt | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt index 3dcd944567bb..b19132e6f89f 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt @@ -29,13 +29,11 @@ import com.owncloud.android.datamodel.ReceiverFlag private const val TAG = "ContextExtensions" @Suppress("TooGenericExceptionCaught") -fun Context.isWebViewAvailable(): Boolean { - return try { - WebViewCompat.getCurrentWebViewPackage(this) != null - } catch (e: Exception) { - Log.w(TAG, "WebView provider cannot be resolved: ${e.message}") - false - } +fun Context.isWebViewAvailable(): Boolean = try { + WebViewCompat.getCurrentWebViewPackage(this) != null +} catch (e: Exception) { + Log.w(TAG, "WebView provider cannot be resolved: ${e.message}") + false } fun Context.hourPlural(hour: Int): String = resources.getQuantityString(R.plurals.hours, hour, hour) diff --git a/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt index 22970ef8a430..13760d3ad242 100644 --- a/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt @@ -22,7 +22,7 @@ class ProxyUtil(private val context: WeakReference) { private const val TAG = "ProxyUtil" } - @SuppressLint("RequiresFeature") + @SuppressLint("RequiresFeature", "ReturnCount") fun applyProxy(onApplied: Runnable) { val context = context.get() ?: return val proxyHost = OwnCloudClientManagerFactory.getProxyHost() From 8ff8cd8be1592f3840558c9e42ad54cdf14ccaad Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Oct 2026 08:27:54 +0200 Subject: [PATCH 4/4] fix kotlin spotless bug Signed-off-by: alperozturk96 --- app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt index 13760d3ad242..84b32b9ae6d6 100644 --- a/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/ProxyUtil.kt @@ -22,7 +22,8 @@ class ProxyUtil(private val context: WeakReference) { private const val TAG = "ProxyUtil" } - @SuppressLint("RequiresFeature", "ReturnCount") + @SuppressLint("RequiresFeature") + @Suppress("ReturnCount") fun applyProxy(onApplied: Runnable) { val context = context.get() ?: return val proxyHost = OwnCloudClientManagerFactory.getProxyHost()