diff --git a/app/src/main/java/org/jellyfin/mobile/MainActivity.kt b/app/src/main/java/org/jellyfin/mobile/MainActivity.kt index b2657f2f..868bc63e 100644 --- a/app/src/main/java/org/jellyfin/mobile/MainActivity.kt +++ b/app/src/main/java/org/jellyfin/mobile/MainActivity.kt @@ -7,6 +7,7 @@ import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import android.view.OrientationEventListener +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.collect @@ -19,6 +20,7 @@ import org.jellyfin.mobile.player.PlayerFragment import org.jellyfin.mobile.utils.Constants import org.jellyfin.mobile.utils.PermissionRequestHelper import org.jellyfin.mobile.utils.SmartOrientationListener +import org.jellyfin.mobile.utils.isWebViewSupported import org.jellyfin.mobile.utils.replaceFragment import org.jellyfin.mobile.viewmodel.MainViewModel import org.jellyfin.mobile.viewmodel.ServerState @@ -53,6 +55,19 @@ class MainActivity : AppCompatActivity() { // Bind player service bindService(Intent(this, RemotePlayerService::class.java), serviceConnection, Service.BIND_AUTO_CREATE) + // Check WebView support + if (!isWebViewSupported()) { + AlertDialog.Builder(this).apply { + setTitle(R.string.dialog_web_view_not_supported) + setMessage(R.string.dialog_web_view_not_supported_message) + setCancelable(false) + setNegativeButton(R.string.dialog_button_close_app) { _, _ -> + finish() + } + }.show() + return + } + // Load UI lifecycleScope.launch { mainViewModel.serverState.collect { state -> diff --git a/app/src/main/java/org/jellyfin/mobile/fragment/WebViewFragment.kt b/app/src/main/java/org/jellyfin/mobile/fragment/WebViewFragment.kt index 2bde78e8..a9551a63 100644 --- a/app/src/main/java/org/jellyfin/mobile/fragment/WebViewFragment.kt +++ b/app/src/main/java/org/jellyfin/mobile/fragment/WebViewFragment.kt @@ -1,6 +1,5 @@ package org.jellyfin.mobile.fragment -import android.annotation.SuppressLint import android.content.Intent import android.graphics.Rect import android.net.Uri @@ -14,6 +13,7 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import androidx.activity.addCallback +import androidx.appcompat.app.AlertDialog import androidx.core.view.ViewCompat import androidx.core.view.doOnNextLayout import androidx.fragment.app.Fragment @@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.webkit.WebResourceErrorCompat import androidx.webkit.WebViewClientCompat +import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import kotlinx.coroutines.launch import org.jellyfin.apiclient.interaction.ApiClient @@ -126,8 +127,41 @@ class WebViewFragment : Fragment(), NativePlayerHost { _webViewBinding = null } - @SuppressLint("SetJavaScriptEnabled") private fun WebView.initialize() { + if (isOutdated()) { // Check WebView version + AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.dialog_web_view_outdated) + setMessage(R.string.dialog_web_view_outdated_message) + setCancelable(false) + + val webViewPackage = WebViewCompat.getCurrentWebViewPackage(context) + if (webViewPackage != null) { + setPositiveButton(R.string.dialog_button_check_for_updates) { _, _ -> + val marketUri = Uri.Builder().apply { + scheme("market") + authority("details") + appendQueryParameter("id", webViewPackage.packageName) + }.build() + val referrerUri = Uri.Builder().apply { + scheme("android-app") + authority(context.packageName) + }.build() + + val marketIntent = Intent(Intent.ACTION_VIEW).apply { + data = marketUri + putExtra(Intent.EXTRA_REFERRER, referrerUri) + } + startActivity(marketIntent) + requireActivity().finish() + } + } + setNegativeButton(R.string.dialog_button_close_app) { _, _ -> + requireActivity().finish() + } + }.show() + return + } + webViewClient = object : WebViewClientCompat() { override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { val url = request.url @@ -186,10 +220,7 @@ class WebViewFragment : Fragment(), NativePlayerHost { } } webChromeClient = WebChromeClient() - settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - } + settings.applyDefault() addJavascriptInterface(NativeInterface(this@WebViewFragment), "NativeInterface") addJavascriptInterface(NativePlayer(this@WebViewFragment), "NativePlayer") addJavascriptInterface(externalPlayer, "ExternalPlayer") diff --git a/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt b/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt index 117ada3d..f47e3e99 100644 --- a/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt +++ b/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt @@ -9,6 +9,7 @@ object Constants { const val APP_INFO_VERSION: String = BuildConfig.VERSION_NAME // Webapp constants + const val MINIMUM_WEB_VIEW_VERSION = 88 const val WEB_CONFIG_PATH = "config.json" const val CAST_SDK_PATH = "cast_sender.js" const val SESSION_CAPABILITIES_PATH = "sessions/capabilities/full" diff --git a/app/src/main/java/org/jellyfin/mobile/utils/WebViewUtils.kt b/app/src/main/java/org/jellyfin/mobile/utils/WebViewUtils.kt new file mode 100644 index 00000000..6d223a71 --- /dev/null +++ b/app/src/main/java/org/jellyfin/mobile/utils/WebViewUtils.kt @@ -0,0 +1,68 @@ +/** + * Taken and adapted from https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt + * + * Copyright 2015 Javier Tomás + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jellyfin.mobile.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import timber.log.Timber + +fun Context.isWebViewSupported(): Boolean { + try { + // May throw android.webkit.WebViewFactory$MissingWebViewPackageException if WebView + // is not installed + CookieManager.getInstance() + } catch (e: Throwable) { + Timber.e(e) + return false + } + + return packageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW) +} + +fun WebView.isOutdated(): Boolean = + getWebViewMajorVersion() < Constants.MINIMUM_WEB_VIEW_VERSION + +private fun WebView.getWebViewMajorVersion(): Int { + return """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString())?.let { match -> + match.groupValues.getOrNull(1)?.toInt() + } ?: 0 +} + +// Based on https://stackoverflow.com/a/29218966 +private fun WebView.getDefaultUserAgentString(): String { + val originalUA: String = settings.userAgentString + + // Next call to getUserAgentString() will get us the default + settings.userAgentString = null + val defaultUserAgentString = settings.userAgentString + + // Revert to original UA string + settings.userAgentString = originalUA + + return defaultUserAgentString +} + +@SuppressLint("SetJavaScriptEnabled") +fun WebSettings.applyDefault() { + javaScriptEnabled = true + domStorageEnabled = true +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7227d65f..ddd79c72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,11 @@ + WebView not supported + WebViews are not supported by this device.\nInstall a compatible WebView implementation or switch to a supported device. + WebView outdated + The WebView implementation used by your device is outdated.\nPlease update it to a compatible version. + Check for updates + Close app + Connect to Server Host Host has an invalid format diff --git a/app/src/proprietary/java/org/jellyfin/mobile/cast/Chromecast.java b/app/src/proprietary/java/org/jellyfin/mobile/cast/Chromecast.java index f3c27ee6..1b405dd3 100644 --- a/app/src/proprietary/java/org/jellyfin/mobile/cast/Chromecast.java +++ b/app/src/proprietary/java/org/jellyfin/mobile/cast/Chromecast.java @@ -579,7 +579,9 @@ public final class Chromecast implements IChromecast { stopRouteScan(callback); sessionStop(callback); - media.destroy(); + if (media != null) { + media.destroy(); + } media = null; if (connection != null) { connection.destroy();