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.java deleted file mode 100644 index b20d447a67ff..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * 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); - } - - 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(); - } - } - - public void onUrlLoaded(String loadedUrl) { - this.url = loadedUrl; - - if (!url.isEmpty()) { - new WebViewUtil().setProxyKKPlus(this.getWebView()); - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - - if (!url.equals(this.getWebView().getUrl())) { - this.getWebView().loadUrl(url); - } - - 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(); - } - } - - public void closeView() { - getWebView().destroy(); - finish(); - } - - public void reload() { - if (getWebView().getVisibility() != View.VISIBLE) { - return; - } - - Optional user = getUser(); - if (!user.isPresent()) { - return; - } - - OCFile file = getFile(); - if (file != null) { - TextEditorLoadUrlTask task = new TextEditorLoadUrlTask(this, user.get(), file, editorUtils); - task.execute(); - } - } - - @Override - protected void bindView() { - binding = RichdocumentsWebviewBinding.inflate(getLayoutInflater()); - } - - @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; - } - }); - - setFile(IntentExtensionsKt.getParcelableArgument(getIntent(), ExternalSiteWebView.EXTRA_FILE, OCFile.class)); - - if (getFile() == null) { - SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document); - finish(); - } - - if (getFile() != null) { - fileName = getFile().getFileName(); - } - - Optional user = getUser(); - if (!user.isPresent()) { - finish(); - return; - } - initLoadingScreen(user.get()); - } - - @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; - } - 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); - } - } - - protected void handleLocalFile(Intent data, int resultCode) { - if (uploadMessage == 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])); - } - - uploadMessage = null; - } - - protected WebView getWebView() { - return binding == null ? null : binding.webView; - } - - protected View getRootView() { - return binding.getRoot(); - } - - protected boolean showToolbarByDefault() { - return false; - } - - protected void initLoadingScreen(final User user) { - setThumbnailView(user); - binding.filename.setText(fileName); - } - - 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); - } - - 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())); - } - } else { - Drawable icon = MimeTypeUtil.getFileTypeIcon(file.getMimeType(), - file.getFileName(), - getApplicationContext(), - viewThemeUtils); - binding.thumbnail.setImageDrawable(icon); - } - } - } - - protected void downloadFile(Uri uri, String filename) { - // 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; - } - - public class MobileInterface { - @JavascriptInterface - public void close() { - runOnUiThread(EditorWebView.this::closeView); - } - - @JavascriptInterface - public void share() { - openShareDialog(); - } - - @JavascriptInterface - public void loaded() { - runOnUiThread(EditorWebView.this::hideLoading); - } - - @JavascriptInterface - public void reload() { - EditorWebView.this.reload(); - } - } - -} 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 new file mode 100644 index 000000000000..c8ece71bd03c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.kt @@ -0,0 +1,278 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * 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.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.activity.result.contract.ActivityResultContracts +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 + + 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) + } + + protected fun hideLoading() { + binding.run { + thumbnail.isVisible = false + filename.isVisible = false + progressBar2.isVisible = false + webView.isVisible = true + } + loadingSnackbar?.dismiss() + } + + fun onUrlLoaded(loadedUrl: String?) { + url = loadedUrl + + if (loadedUrl.isNullOrEmpty()) { + SnackbarUtil.show(this, R.string.richdocuments_failed_to_load_document) + finish() + return + } + + lifecycleScope.launch { + WebViewUtil().setProxyKKPlus(webView) + delay(PROXY_SETUP_DELAY) + + if (loadedUrl != webView.url) { + webView.loadUrl(loadedUrl) + } + + delay(LOADING_TIMEOUT) + if (!webView.isVisible) { + showLoadingTimeoutSnackbar() + } + } + } + + 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() + } + + private fun closeView() { + webView.destroy() + finish() + } + + private fun reload() { + val user = user.orElse(null) + val file = file + if (!webView.isVisible || user == null || file == null) { + return + } + + TextEditorLoadUrlTask(this, user, file, editorUtils).execute() + } + + override fun bindView() { + binding = RichdocumentsWebviewBinding.inflate(layoutInflater) + } + + override val isWebViewBound: Boolean + get() = ::binding.isInitialized + + override fun postOnCreate() { + super.postOnCreate() + + viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar2, ColorRole.PRIMARY) + webView.webChromeClient = createFileChooserClient() + + 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 + + val user = user.orElse(null) ?: run { + finish() + return + } + setThumbnailView(file, user) + binding.filename.text = fileName + } + + 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 try { + fileChooserLauncher.launch(intent) + true + } catch (_: ActivityNotFoundException) { + uploadMessage = null + SnackbarUtil.show(this@EditorWebView, R.string.editor_web_view_cannot_open_file) + false + } + } + } + + 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 } + } + + private fun completeFileChooser(uris: Array?) { + uploadMessage?.onReceiveValue(uris) + uploadMessage = null + } + + override val webView: WebView + get() = binding.webView + + override val rootView: View + get() = 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) + } + + 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 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) + } + + 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 { + thumbnail + } + binding.thumbnail.setImageBitmap(bitmap) + } + + if (file.isPNG()) { + binding.thumbnail.setBackgroundColor(getColor(R.color.bg_default)) + } + } + + 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. + lifecycleScope.launch { + RichDocumentDownloader(this@EditorWebView).download(uri, filename, webView.settings.userAgentString) + } + } + + open inner class MobileInterface { + @JavascriptInterface + fun close() { + lifecycleScope.launch { closeView() } + } + + @JavascriptInterface + fun share() { + openShareDialog() + } + + @JavascriptInterface + fun loaded() { + lifecycleScope.launch { hideLoading() } + } + + @JavascriptInterface + fun reload() { + this@EditorWebView.reload() + } + } + + companion object { + 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 deleted file mode 100644 index a35ec658eeac..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-FileCopyrightText: 2017 Tobias Kaminsky - * 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; - -/** - * 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"; - - private static final String TAG = ExternalSiteWebView.class.getSimpleName(); - - protected boolean showToolbar = true; - private ExternalsiteWebviewBinding binding; - private boolean showSidebar; - String url; - - @Override - protected final void onCreate(Bundle savedInstanceState) { - 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); - - setContentView(getRootView()); - - postOnCreate(); - } - - protected void postOnCreate() { - final WebSettings webSettings = getWebView().getSettings(); - - getWebView().setFocusable(true); - getWebView().setFocusableInTouchMode(true); - getWebView().setClickable(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); - } - - // setup toolbar - if (showToolbar) { - setupToolbar(); - } else { - if (findViewById(R.id.appbar) != null) { - findViewById(R.id.appbar).setVisibility(View.GONE); - } - } - - 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); - } - 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); - } - }); - } - - 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); - } - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!request.isRedirect()) { - IntentUtil.startLinkIntent(self, request.getUrl()); - return true; - } - return false; - } - }); - - new WebViewUtil().setProxyKKPlus(getWebView()); - getWebView().loadUrl(url); - } - - @Override - protected void onDestroy() { - WebView webView = getWebView(); - if (webView != null) { - webView.destroy(); - } - super.onDestroy(); - } - - protected void bindView() { - binding = ExternalsiteWebviewBinding.inflate(getLayoutInflater()); - } - - protected boolean showToolbarByDefault() { - return true; - } - - protected View getRootView() { - return binding.getRoot(); - } - - @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 void setupActionBar(String title) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - viewThemeUtils.files.themeActionBar(this, actionBar, title); - - 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); - } - } - - protected WebView getWebView() { - return binding == null ? null : binding.webView; - } -} 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 new file mode 100644 index 000000000000..4ce4b705a7a6 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.kt @@ -0,0 +1,211 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2017 Tobias Kaminsky + * 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.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 + */ +open class ExternalSiteWebView : FileActivity() { + private lateinit var binding: ExternalsiteWebviewBinding + + private var showToolbar = true + private var showSidebar = false + protected var url: String? = null + + private val isDebuggable: Boolean + get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + + 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() + readIntentExtras() + window?.requestFeature(Window.FEATURE_PROGRESS) + + super.onCreate(savedInstanceState) + setContentView(rootView) + postOnCreate() + } + + private fun readIntentExtras() { + url = intent.getStringExtra(EXTRA_URL) + showToolbar = intent.getBooleanExtra(EXTRA_SHOW_TOOLBAR, showToolbarByDefault()) + showSidebar = intent.getBooleanExtra(EXTRA_SHOW_SIDEBAR, false) + } + + 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 (isDebuggable || resources.getBoolean(R.bool.is_beta)) { + Log_OC.d(this, "Enable debug for webView") + WebView.setWebContentsDebuggingEnabled(true) + } + + setupToolbarAndDrawer() + setupWebSettings(webView.settings) + webView.webViewClient = createWebViewClient() + + WebViewUtil().setProxyKKPlus(webView) + url?.let { webView.loadUrl(it) } + } + + private fun setupToolbarAndDrawer() { + if (showToolbar) { + setupToolbar() + } else { + findViewById(R.id.appbar)?.isVisible = false + } + + setupDrawer(R.id.nav_view) + + if (!showSidebar) { + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + + val title = intent.getStringExtra(EXTRA_TITLE) + if (!title.isNullOrEmpty()) { + setupActionBar(title) + } + } + + 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 fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest): Boolean { + if (request.isRedirect) { + return false + } + + IntentUtil.startLinkIntent(this@ExternalSiteWebView, request.url) + return true + } + } + + override fun onDestroy() { + if (isWebViewBound) { + webView.destroy() + } + super.onDestroy() + } + + protected open val isWebViewBound: Boolean + get() = ::binding.isInitialized + + protected open fun bindView() { + binding = ExternalsiteWebviewBinding.inflate(layoutInflater) + } + + protected open fun showToolbarByDefault(): Boolean = true + + protected open val rootView: View + get() = binding.root + + protected open val webView: WebView + get() = binding.webView + + @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") + @SuppressLint("SetJavaScriptEnabled") + 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 fun setupActionBar(title: String) { + val actionBar = supportActionBar ?: return + viewThemeUtils.files.themeActionBar(this, actionBar, title) + + if (showSidebar) { + actionBar.setDisplayHomeAsUpEnabled(true) + } else { + setDrawerIndicatorEnabled(false) + } + } + + 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 + } + + 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) }