From 533c1d859e2d8b6823a3f8b9f0c847cbddf699e9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 30 Sep 2026 15:19:15 +0200 Subject: [PATCH 1/5] Rename .java to .kt Signed-off-by: alperozturk96 --- .../android/ui/activity/{EditorWebView.java => EditorWebView.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/activity/{EditorWebView.java => EditorWebView.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java rename to app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt From 56fc650ff8f45d8dfc5f9dc41831806b562d1437 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 30 Sep 2026 15:19:16 +0200 Subject: [PATCH 2/5] refactor(editor-web-view): kotlin Signed-off-by: alperozturk96 --- .../android/ui/activity/EditorWebView.kt | 442 ++++++++---------- .../ui/activity/ExternalSiteWebView.java | 9 +- 2 files changed, 205 insertions(+), 246 deletions(-) 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 b20d447a67ff..59029275e0a7 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 @@ -6,319 +6,275 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.activity; - -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Handler; -import android.view.View; -import android.webkit.JavascriptInterface; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebView; - -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.SnackbarUtil; -import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.extensions.IntentExtensionsKt; -import com.nextcloud.utils.thumbnail.VideoOverlayGenerator; -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.datamodel.SyncedFolderProvider; -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 java.util.ArrayList; -import java.util.Optional; - -import javax.inject.Inject; - -public abstract class EditorWebView extends ExternalSiteWebView { - public static final int REQUEST_LOCAL_FILE = 101; - public ValueCallback uploadMessage; - protected Snackbar loadingSnackbar; - - protected String fileName; - - RichdocumentsWebviewBinding binding; - - @Inject SyncedFolderProvider syncedFolderProvider; - - protected void loadUrl(String url) { - onUrlLoaded(url); +package com.owncloud.android.ui.activity + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.view.View +import android.webkit.JavascriptInterface +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebChromeClient.FileChooserParams +import android.webkit.WebView +import androidx.core.view.isVisible +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.SnackbarUtil +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.getSmallThumbnail +import com.nextcloud.utils.extensions.isPNG +import com.nextcloud.utils.thumbnail.VideoOverlayGenerator +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.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 + +abstract class EditorWebView : ExternalSiteWebView() { + private lateinit var binding: RichdocumentsWebviewBinding + + protected lateinit var fileName: String + + private var uploadMessage: ValueCallback>? = null + private var loadingSnackbar: Snackbar? = null + + protected open fun loadUrl(url: String?) { + onUrlLoaded(url) } - protected void hideLoading() { - binding.thumbnail.setVisibility(View.GONE); - binding.filename.setVisibility(View.GONE); - binding.progressBar2.setVisibility(View.GONE); - getWebView().setVisibility(View.VISIBLE); - - if (loadingSnackbar != null) { - loadingSnackbar.dismiss(); + protected fun hideLoading() { + binding.run { + thumbnail.isVisible = false + filename.isVisible = false + progressBar2.isVisible = false + webView.isVisible = true } + loadingSnackbar?.dismiss() } - public void onUrlLoaded(String loadedUrl) { - this.url = loadedUrl; + fun onUrlLoaded(loadedUrl: String?) { + url = loadedUrl - if (!url.isEmpty()) { - new WebViewUtil().setProxyKKPlus(this.getWebView()); - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } + if (loadedUrl.isNullOrEmpty()) { + SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document) + finish() + return + } - if (!url.equals(this.getWebView().getUrl())) { - this.getWebView().loadUrl(url); + lifecycleScope.launch { + WebViewUtil().setProxyKKPlus(webView) + delay(PROXY_SETUP_DELAY) + + if (loadedUrl != webView.url) { + webView.loadUrl(loadedUrl) } - new Handler().postDelayed(() -> { - if (this.getWebView().getVisibility() != View.VISIBLE) { - Snackbar snackbar = SnackbarUtil.create(findViewById(android.R.id.content), - R.string.timeout_richDocuments, - Snackbar.LENGTH_INDEFINITE); - if (snackbar != null) { - snackbar.setAction(R.string.common_cancel, v -> closeView()); - viewThemeUtils.material.themeSnackbar(snackbar); - setLoadingSnackbar(snackbar); - snackbar.show(); - } - } - }, 10 * 1000); - } else { - SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document); - finish(); + delay(LOADING_TIMEOUT) + if (!webView.isVisible) { + showLoadingTimeoutSnackbar() + } } } - public void closeView() { - getWebView().destroy(); - finish(); + private fun showLoadingTimeoutSnackbar() { + val snackbar = SnackbarUtil.create( + findViewById(android.R.id.content), + R.string.timeout_richDocuments, + Snackbar.LENGTH_INDEFINITE + ) ?: return + + snackbar.setAction(R.string.common_cancel) { closeView() } + viewThemeUtils.material.themeSnackbar(snackbar) + loadingSnackbar = snackbar + snackbar.show() } - public void reload() { - if (getWebView().getVisibility() != View.VISIBLE) { - return; - } + private fun closeView() { + webView.destroy() + finish() + } - Optional user = getUser(); - if (!user.isPresent()) { - return; + private fun reload() { + val user = user.orElse(null) + val file = file + if (!webView.isVisible || user == null || file == null) { + return } - OCFile file = getFile(); - if (file != null) { - TextEditorLoadUrlTask task = new TextEditorLoadUrlTask(this, user.get(), file, editorUtils); - task.execute(); - } + TextEditorLoadUrlTask(this, user, file, editorUtils).execute() } - @Override - protected void bindView() { - binding = RichdocumentsWebviewBinding.inflate(getLayoutInflater()); + override fun bindView() { + binding = RichdocumentsWebviewBinding.inflate(layoutInflater) } - @Override - protected void postOnCreate() { - super.postOnCreate(); - - viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar2, ColorRole.PRIMARY); - - getWebView().setWebChromeClient(new WebChromeClient() { - final EditorWebView activity = EditorWebView.this; - - @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, - FileChooserParams fileChooserParams) { - if (uploadMessage != null) { - uploadMessage.onReceiveValue(null); - uploadMessage = null; - } - - activity.uploadMessage = filePathCallback; - - Intent intent = fileChooserParams.createIntent(); - intent.setType("image/*"); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - try { - activity.startActivityForResult(intent, REQUEST_LOCAL_FILE); - } catch (ActivityNotFoundException e) { - uploadMessage = null; - SnackbarUtil.show(EditorWebView.this, R.string.editor_web_view_cannot_open_file); - return false; - } - - return true; - } - }); + override fun isWebViewBound(): Boolean = ::binding.isInitialized - setFile(IntentExtensionsKt.getParcelableArgument(getIntent(), ExternalSiteWebView.EXTRA_FILE, OCFile.class)); + override fun postOnCreate() { + super.postOnCreate() - if (getFile() == null) { - SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document); - finish(); - } + viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar2, ColorRole.PRIMARY) + webView.webChromeClient = createFileChooserClient() - if (getFile() != null) { - fileName = getFile().getFileName(); + file = intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + val file = file ?: run { + SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document) + finish() + return } + fileName = file.fileName - Optional user = getUser(); - if (!user.isPresent()) { - finish(); - return; + val user = user.orElse(null) ?: run { + finish() + return } - initLoadingScreen(user.get()); + setThumbnailView(file, user) + binding.filename.text = fileName } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (RESULT_OK != resultCode) { - if (requestCode == REQUEST_LOCAL_FILE) { - this.uploadMessage.onReceiveValue(null); - this.uploadMessage = null; + private fun createFileChooserClient() = object : WebChromeClient() { + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams + ): Boolean { + completeFileChooser(null) + uploadMessage = filePathCallback + + val intent = fileChooserParams.createIntent().apply { + type = IMAGE_MIME_TYPE_FILTER + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) } - return; - } - handleActivityResult(requestCode, resultCode, data); - - super.onActivityResult(requestCode, resultCode, data); - } - - protected void handleActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_LOCAL_FILE) { - handleLocalFile(data, resultCode); + return try { + startActivityForResult(intent, REQUEST_LOCAL_FILE) + true + } catch (_: ActivityNotFoundException) { + uploadMessage = null + SnackbarUtil.show(this@EditorWebView, R.string.editor_web_view_cannot_open_file) + false + } } } - protected void handleLocalFile(Intent data, int resultCode) { - if (uploadMessage == null) { - return; + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode != RESULT_OK) { + if (requestCode == REQUEST_LOCAL_FILE) { + completeFileChooser(null) + } + return } - if (data.getClipData() == null) { - // one file - uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data)); - } else { - ArrayList uris = new ArrayList<>(); - // multiple files - for (int i = 0; i < data.getClipData().getItemCount(); i++) { - ClipData.Item item = data.getClipData().getItemAt(i); - uris.add(item.getUri()); - } - - uploadMessage.onReceiveValue(uris.toArray(new Uri[0])); + if (requestCode == REQUEST_LOCAL_FILE) { + completeFileChooser(parseChosenFiles(resultCode, data)) } - uploadMessage = null; + super.onActivityResult(requestCode, resultCode, data) } - protected WebView getWebView() { - return binding == null ? null : binding.webView; + private fun parseChosenFiles(resultCode: Int, data: Intent?): Array? { + val clipData = data?.clipData ?: return FileChooserParams.parseResult(resultCode, data) + return Array(clipData.itemCount) { clipData.getItemAt(it).uri } } - protected View getRootView() { - return binding.getRoot(); + private fun completeFileChooser(uris: Array?) { + uploadMessage?.onReceiveValue(uris) + uploadMessage = null } - protected boolean showToolbarByDefault() { - return false; + override fun getWebView(): WebView = binding.webView + + override fun getRootView(): View = binding.root + + override fun showToolbarByDefault(): Boolean = false + + private fun openShareDialog() { + val intent = Intent(this, ShareActivity::class.java).apply { + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_USER, user.orElseThrow { RuntimeException() }) + } + startActivity(intent) } - protected void initLoadingScreen(final User user) { - setThumbnailView(user); - binding.filename.setText(fileName); + private fun setThumbnailView(file: OCFile, user: User) { + // Todo minimize: only icon by mimetype + when { + file.isFolder -> binding.thumbnail.setImageDrawable(getFolderIcon(file, user)) + + MimeTypeUtil.isImageOrVideo(file) && file.remoteId != null -> showCachedThumbnail(file) + + else -> binding.thumbnail.setImageDrawable( + MimeTypeUtil.getFileTypeIcon(file.mimeType, file.fileName, applicationContext, viewThemeUtils) + ) + } } - private void openShareDialog() { - Intent intent = new Intent(this, ShareActivity.class); - intent.putExtra(FileActivity.EXTRA_FILE, getFile()); - intent.putExtra(FileActivity.EXTRA_USER, getUser().orElseThrow(RuntimeException::new)); - startActivity(intent); + private fun getFolderIcon(file: OCFile, user: User): Drawable { + val isAutoUploadFolder = SyncedFolderObserver.isAutoUploadFolder(file, user) + val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder) + return MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled, overlayIconId, this, viewThemeUtils) } - protected void setThumbnailView(final User user) { - // Todo minimize: only icon by mimetype - OCFile file = getFile(); - if (file.isFolder()) { - boolean isAutoUploadFolder = SyncedFolderObserver.INSTANCE.isAutoUploadFolder(file, user); - - Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); - LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils); - binding.thumbnail.setImageDrawable(drawable); - } else { - if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) { - // Thumbnail in cache? - Bitmap thumbnail = FileExtensionsKt.getSmallThumbnail(file); - - if (thumbnail != null && !file.isUpdateThumbnailNeeded()) { - if (MimeTypeUtil.isVideo(file)) { - Bitmap withOverlay = VideoOverlayGenerator.addOverlay(thumbnail, this); - binding.thumbnail.setImageBitmap(withOverlay); - } else { - binding.thumbnail.setImageBitmap(thumbnail); - } - } - - if ("image/png".equalsIgnoreCase(file.getMimeType())) { - binding.thumbnail.setBackgroundColor(getResources().getColor(R.color.bg_default, getTheme())); - } + private fun showCachedThumbnail(file: OCFile) { + val thumbnail = file.getSmallThumbnail() + if (thumbnail != null && !file.isUpdateThumbnailNeeded) { + val bitmap = if (MimeTypeUtil.isVideo(file)) { + VideoOverlayGenerator.addOverlay(thumbnail, this) } else { - Drawable icon = MimeTypeUtil.getFileTypeIcon(file.getMimeType(), - file.getFileName(), - getApplicationContext(), - viewThemeUtils); - binding.thumbnail.setImageDrawable(icon); + thumbnail } + binding.thumbnail.setImageBitmap(bitmap) + } + + if (file.isPNG()) { + binding.thumbnail.setBackgroundColor(getColor(R.color.bg_default)) } } - protected void downloadFile(Uri uri, String filename) { + protected fun downloadFile(uri: Uri, filename: String?) { // downloadAs is invoked from the WebView JavaScript bridge thread, but WebView methods // (getSettings) must run on the main thread, so read the user agent there. - runOnUiThread(() -> { - String userAgent = getWebView().getSettings().getUserAgentString(); - new RichDocumentDownloader(this).download(uri, filename, userAgent); - }); - } - - public void setLoadingSnackbar(Snackbar loadingSnackbar) { - this.loadingSnackbar = loadingSnackbar; + lifecycleScope.launch { + RichDocumentDownloader(this@EditorWebView).download(uri, filename, webView.settings.userAgentString) + } } - public class MobileInterface { + open inner class MobileInterface { @JavascriptInterface - public void close() { - runOnUiThread(EditorWebView.this::closeView); + fun close() { + lifecycleScope.launch { closeView() } } @JavascriptInterface - public void share() { - openShareDialog(); + fun share() { + openShareDialog() } @JavascriptInterface - public void loaded() { - runOnUiThread(EditorWebView.this::hideLoading); + fun loaded() { + lifecycleScope.launch { hideLoading() } } @JavascriptInterface - public void reload() { - EditorWebView.this.reload(); + fun reload() { + this@EditorWebView.reload() } } + companion object { + private const val REQUEST_LOCAL_FILE = 101 + private const val IMAGE_MIME_TYPE_FILTER = "image/*" + private val PROXY_SETUP_DELAY = 1.seconds + private val LOADING_TIMEOUT = 10.seconds + } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java index a35ec658eeac..2d0e2caa44c0 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java @@ -162,13 +162,16 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request @Override protected void onDestroy() { - WebView webView = getWebView(); - if (webView != null) { - webView.destroy(); + if (isWebViewBound()) { + getWebView().destroy(); } super.onDestroy(); } + protected boolean isWebViewBound() { + return binding != null; + } + protected void bindView() { binding = ExternalsiteWebviewBinding.inflate(getLayoutInflater()); } From fa5f16d25a132c29c5273cf742c88f588d32da32 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 30 Sep 2026 15:29:32 +0200 Subject: [PATCH 3/5] Rename .java to .kt Signed-off-by: alperozturk96 --- .../activity/{ExternalSiteWebView.java => ExternalSiteWebView.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/activity/{ExternalSiteWebView.java => ExternalSiteWebView.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java rename to app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt From 1f35cc0faf5d3f50b5ab3a6e5adf61de7451952a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 30 Sep 2026 15:29:32 +0200 Subject: [PATCH 4/5] refactor(editor-site-web-view): kotlin Signed-off-by: alperozturk96 --- .../android/ui/activity/EditorWebView.kt | 9 +- .../ui/activity/ExternalSiteWebView.kt | 331 ++++++++---------- ...ooseRichDocumentsTemplateDialogFragment.kt | 3 +- .../ui/dialog/ChooseTemplateDialogFragment.kt | 3 +- 4 files changed, 155 insertions(+), 191 deletions(-) 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 59029275e0a7..242d73118f02 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 @@ -118,7 +118,8 @@ abstract class EditorWebView : ExternalSiteWebView() { binding = RichdocumentsWebviewBinding.inflate(layoutInflater) } - override fun isWebViewBound(): Boolean = ::binding.isInitialized + override val isWebViewBound: Boolean + get() = ::binding.isInitialized override fun postOnCreate() { super.postOnCreate() @@ -192,9 +193,11 @@ abstract class EditorWebView : ExternalSiteWebView() { uploadMessage = null } - override fun getWebView(): WebView = binding.webView + override val webView: WebView + get() = binding.webView - override fun getRootView(): View = binding.root + override val rootView: View + get() = binding.root override fun showToolbarByDefault(): Boolean = false 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 2d0e2caa44c0..4ce4b705a7a6 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 @@ -6,247 +6,206 @@ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ - -package com.owncloud.android.ui.activity; - -import android.annotation.SuppressLint; -import android.content.pm.ApplicationInfo; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.MenuItem; -import android.view.View; -import android.view.Window; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.widget.ProgressBar; - -import com.nextcloud.client.utils.IntentUtil; -import com.nextcloud.utils.RawResourceReader; -import com.nextcloud.utils.SnackbarUtil; -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 androidx.appcompat.app.ActionBar; -import androidx.drawerlayout.widget.DrawerLayout; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +package com.owncloud.android.ui.activity + +import android.annotation.SuppressLint +import android.content.pm.ApplicationInfo +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.Window +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.core.view.isVisible +import androidx.drawerlayout.widget.DrawerLayout +import com.nextcloud.client.utils.IntentUtil +import com.nextcloud.utils.RawResourceReader +import com.nextcloud.utils.SnackbarUtil +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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings /** * This activity shows an URL as a web view */ -public class ExternalSiteWebView extends FileActivity { - public static final String EXTRA_TITLE = "TITLE"; - public static final String EXTRA_URL = "URL"; - public static final String EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR"; - public static final String EXTRA_SHOW_TOOLBAR = "SHOW_TOOLBAR"; - public static final String EXTRA_TEMPLATE = "TEMPLATE"; +open class ExternalSiteWebView : FileActivity() { + private lateinit var binding: ExternalsiteWebviewBinding - private static final String TAG = ExternalSiteWebView.class.getSimpleName(); + private var showToolbar = true + private var showSidebar = false + protected var url: String? = null - protected boolean showToolbar = true; - private ExternalsiteWebviewBinding binding; - private boolean showSidebar; - String url; + private val isDebuggable: Boolean + get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 - @Override - protected final void onCreate(Bundle savedInstanceState) { - Log_OC.v(TAG, "onCreate() start"); + final override fun onCreate(savedInstanceState: Bundle?) { + Log_OC.v(TAG, "onCreate() start") if (!WebViewUtil.available(this)) { - super.onCreate(savedInstanceState); - SnackbarUtil.show(this, R.string.webview_not_available); - finish(); - return; - } - - bindView(); - showToolbar = showToolbarByDefault(); - - Bundle extras = getIntent().getExtras(); - url = getIntent().getExtras().getString(EXTRA_URL); - if (extras.containsKey(EXTRA_SHOW_TOOLBAR)) { - showToolbar = extras.getBoolean(EXTRA_SHOW_TOOLBAR); - } - - showSidebar = extras.getBoolean(EXTRA_SHOW_SIDEBAR); - - // show progress - Window window = getWindow(); - if (window != null) { - window.requestFeature(Window.FEATURE_PROGRESS); + super.onCreate(savedInstanceState) + SnackbarUtil.show(this, R.string.webview_not_available) + finish() + return } - super.onCreate(savedInstanceState); - - setContentView(getRootView()); + bindView() + readIntentExtras() + window?.requestFeature(Window.FEATURE_PROGRESS) - postOnCreate(); + super.onCreate(savedInstanceState) + setContentView(rootView) + postOnCreate() } - protected void postOnCreate() { - final WebSettings webSettings = getWebView().getSettings(); + private fun readIntentExtras() { + url = intent.getStringExtra(EXTRA_URL) + showToolbar = intent.getBooleanExtra(EXTRA_SHOW_TOOLBAR, showToolbarByDefault()) + showSidebar = intent.getBooleanExtra(EXTRA_SHOW_SIDEBAR, false) + } - getWebView().setFocusable(true); - getWebView().setFocusableInTouchMode(true); - getWebView().setClickable(true); + protected open fun postOnCreate() { + webView.run { + isFocusable = true + isFocusableInTouchMode = true + isClickable = true + } // allow debugging (when building the debug version); see details in // https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews - if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 || - getResources().getBoolean(R.bool.is_beta)) { - Log_OC.d(this, "Enable debug for webView"); - WebView.setWebContentsDebuggingEnabled(true); + if (isDebuggable || resources.getBoolean(R.bool.is_beta)) { + Log_OC.d(this, "Enable debug for webView") + WebView.setWebContentsDebuggingEnabled(true) } - // setup toolbar + setupToolbarAndDrawer() + setupWebSettings(webView.settings) + webView.webViewClient = createWebViewClient() + + WebViewUtil().setProxyKKPlus(webView) + url?.let { webView.loadUrl(it) } + } + + private fun setupToolbarAndDrawer() { if (showToolbar) { - setupToolbar(); + setupToolbar() } else { - if (findViewById(R.id.appbar) != null) { - findViewById(R.id.appbar).setVisibility(View.GONE); - } + findViewById(R.id.appbar)?.isVisible = false } - setupDrawer(R.id.nav_view); + setupDrawer(R.id.nav_view) if (!showSidebar) { - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - } - - String title = getIntent().getExtras().getString(EXTRA_TITLE); - if (!TextUtils.isEmpty(title)) { - setupActionBar(title); + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) } - setupWebSettings(webSettings); - - final ProgressBar progressBar = findViewById(R.id.progressBar); - if (progressBar != null) { - getWebView().setWebChromeClient(new WebChromeClient() { - public void onProgressChanged(WebView view, int progress) { - progressBar.setProgress(progress * 1000); - } - }); + val title = intent.getStringExtra(EXTRA_TITLE) + if (!title.isNullOrEmpty()) { + setupActionBar(title) } + } - final ExternalSiteWebView self = this; - getWebView().setWebViewClient(new NextcloudWebViewClient(getSupportFragmentManager()) { - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - String customError = RawResourceReader.readText(getResources(), R.raw.custom_error); - - if (!customError.isEmpty()) { - getWebView().loadData(customError, "text/html; charset=UTF-8", null); - } + private fun createWebViewClient() = object : NextcloudWebViewClient(supportFragmentManager) { + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + val customError = RawResourceReader.readText(resources, R.raw.custom_error) + if (customError.isNotEmpty()) { + webView.loadData(customError, CUSTOM_ERROR_MIME_TYPE, null) } + } - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!request.isRedirect()) { - IntentUtil.startLinkIntent(self, request.getUrl()); - return true; - } - return false; + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean { + if (request.isRedirect) { + return false } - }); - new WebViewUtil().setProxyKKPlus(getWebView()); - getWebView().loadUrl(url); + IntentUtil.startLinkIntent(this@ExternalSiteWebView, request.url) + return true + } } - @Override - protected void onDestroy() { - if (isWebViewBound()) { - getWebView().destroy(); + override fun onDestroy() { + if (isWebViewBound) { + webView.destroy() } - super.onDestroy(); + super.onDestroy() } - protected boolean isWebViewBound() { - return binding != null; - } + protected open val isWebViewBound: Boolean + get() = ::binding.isInitialized - protected void bindView() { - binding = ExternalsiteWebviewBinding.inflate(getLayoutInflater()); + protected open fun bindView() { + binding = ExternalsiteWebviewBinding.inflate(layoutInflater) } - protected boolean showToolbarByDefault() { - return true; - } + protected open fun showToolbarByDefault(): Boolean = true - protected View getRootView() { - return binding.getRoot(); - } + protected open val rootView: View + get() = binding.root + + protected open val webView: WebView + get() = binding.webView @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") @SuppressLint("SetJavaScriptEnabled") - private void setupWebSettings(WebSettings webSettings) { - // enable zoom - webSettings.setSupportZoom(true); - webSettings.setBuiltInZoomControls(true); - webSettings.setDisplayZoomControls(false); - - // Non-responsive webs are zoomed out when loaded - webSettings.setUseWideViewPort(true); - webSettings.setLoadWithOverviewMode(true); - - // user agent - webSettings.setUserAgentString(MainApp.getUserAgent()); - - // do not store private data - webSettings.setSaveFormData(false); - - // disable local file access - webSettings.setAllowFileAccess(false); - - // enable javascript - webSettings.setJavaScriptEnabled(true); - webSettings.setDomStorageEnabled(true); - - // caching disabled in debug mode - if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { - webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); + private fun setupWebSettings(webSettings: WebSettings) { + webSettings.run { + setSupportZoom(true) + builtInZoomControls = true + displayZoomControls = false + + // Non-responsive webs are zoomed out when loaded + useWideViewPort = true + loadWithOverviewMode = true + + userAgentString = MainApp.getUserAgent() + saveFormData = false + allowFileAccess = false + javaScriptEnabled = true + domStorageEnabled = true + + if (isDebuggable) { + cacheMode = WebSettings.LOAD_NO_CACHE + } } } - private void setupActionBar(String title) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - viewThemeUtils.files.themeActionBar(this, actionBar, title); + private fun setupActionBar(title: String) { + val actionBar = supportActionBar ?: return + viewThemeUtils.files.themeActionBar(this, actionBar, title) - if (showSidebar) { - actionBar.setDisplayHomeAsUpEnabled(true); - } else { - setDrawerIndicatorEnabled(false); - } + if (showSidebar) { + actionBar.setDisplayHomeAsUpEnabled(true) + } else { + setDrawerIndicatorEnabled(false) } } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - if (showSidebar) { - if (isDrawerOpen()) { - closeDrawer(); - } else { - openDrawer(); - } - } else { - finish(); - } - return true; - } else { - return super.onOptionsItemSelected(item); + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId != android.R.id.home) { + return super.onOptionsItemSelected(item) + } + + when { + !showSidebar -> finish() + isDrawerOpen -> closeDrawer() + else -> openDrawer() } + return true } - protected WebView getWebView() { - return binding == null ? null : binding.webView; + companion object { + const val EXTRA_TITLE = "TITLE" + const val EXTRA_URL = "URL" + const val EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR" + const val EXTRA_SHOW_TOOLBAR = "SHOW_TOOLBAR" + const val EXTRA_TEMPLATE = "TEMPLATE" + + private const val CUSTOM_ERROR_MIME_TYPE = "text/html; charset=UTF-8" + private val TAG = ExternalSiteWebView::class.java.simpleName } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index 3012216ac1a3..d68ee558cd38 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt @@ -41,6 +41,7 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.ExternalSiteWebView +import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.RichDocumentsEditorWebView import com.owncloud.android.ui.adapter.RichDocumentsTemplateAdapter import com.owncloud.android.ui.dialog.IndeterminateProgressDialog.Companion.newInstance @@ -373,7 +374,7 @@ class ChooseRichDocumentsTemplateDialogFragment : val intent = Intent(MainApp.getAppContext(), RichDocumentsEditorWebView::class.java).apply { putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora") putExtra(ExternalSiteWebView.EXTRA_URL, url) - putExtra(ExternalSiteWebView.EXTRA_FILE, file) + putExtra(FileActivity.EXTRA_FILE, file) putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) putExtra(ExternalSiteWebView.EXTRA_TEMPLATE, template) } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index 8bd8393cf1c8..3c87d97dc6b6 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -47,6 +47,7 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.ui.activity.ExternalSiteWebView +import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.TextEditorWebView import com.owncloud.android.ui.adapter.TemplateAdapter import com.owncloud.android.utils.FileStorageUtils @@ -347,7 +348,7 @@ class ChooseTemplateDialogFragment : val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java).apply { putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text") putExtra(ExternalSiteWebView.EXTRA_URL, url) - putExtra(ExternalSiteWebView.EXTRA_FILE, file) + putExtra(FileActivity.EXTRA_FILE, file) putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) } From ce74cf3934fbdc9c646387852114f86a2b17caac Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 30 Sep 2026 15:32:49 +0200 Subject: [PATCH 5/5] use ActivityResultContracts Signed-off-by: alperozturk96 --- .../android/ui/activity/EditorWebView.kt | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) 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 242d73118f02..c8ece71bd03c 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 @@ -18,6 +18,7 @@ import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebChromeClient.FileChooserParams import android.webkit.WebView +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar @@ -48,6 +49,16 @@ abstract class EditorWebView : ExternalSiteWebView() { private var uploadMessage: ValueCallback>? = null private var loadingSnackbar: Snackbar? = null + private val fileChooserLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val uris = if (result.resultCode == RESULT_OK) { + parseChosenFiles(result.resultCode, result.data) + } else { + null + } + completeFileChooser(uris) + } + protected open fun loadUrl(url: String?) { onUrlLoaded(url) } @@ -158,7 +169,7 @@ abstract class EditorWebView : ExternalSiteWebView() { } return try { - startActivityForResult(intent, REQUEST_LOCAL_FILE) + fileChooserLauncher.launch(intent) true } catch (_: ActivityNotFoundException) { uploadMessage = null @@ -168,21 +179,6 @@ abstract class EditorWebView : ExternalSiteWebView() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode != RESULT_OK) { - if (requestCode == REQUEST_LOCAL_FILE) { - completeFileChooser(null) - } - return - } - - if (requestCode == REQUEST_LOCAL_FILE) { - completeFileChooser(parseChosenFiles(resultCode, data)) - } - - super.onActivityResult(requestCode, resultCode, data) - } - private fun parseChosenFiles(resultCode: Int, data: Intent?): Array? { val clipData = data?.clipData ?: return FileChooserParams.parseResult(resultCode, data) return Array(clipData.itemCount) { clipData.getItemAt(it).uri } @@ -275,7 +271,6 @@ abstract class EditorWebView : ExternalSiteWebView() { } companion object { - private const val REQUEST_LOCAL_FILE = 101 private const val IMAGE_MIME_TYPE_FILTER = "image/*" private val PROXY_SETUP_DELAY = 1.seconds private val LOADING_TIMEOUT = 10.seconds