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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@ package dev.hotwire.core.turbo.errors
/**
* Represents all possible errors received when attempting to load a page.
*/
sealed interface VisitError
sealed interface VisitError {
fun description() = when (this) {
is HttpError -> reasonPhrase
is LoadError -> description
is WebError -> description
is WebSslError -> description
}
}
37 changes: 32 additions & 5 deletions core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import android.content.Context
import android.graphics.Bitmap
import android.net.http.SslError
import android.util.SparseArray
import android.webkit.*
import android.webkit.HttpAuthHandler
import android.webkit.JavascriptInterface
import android.webkit.RenderProcessGoneDetail
import android.webkit.SslErrorHandler
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebResourceErrorCompat
Expand All @@ -17,20 +24,26 @@ import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.files.delegates.FileChooserDelegate
import dev.hotwire.core.files.delegates.GeolocationPermissionDelegate
import dev.hotwire.core.logging.logEvent
import dev.hotwire.core.logging.logWarning
import dev.hotwire.core.turbo.errors.HttpError
import dev.hotwire.core.turbo.errors.LoadError
import dev.hotwire.core.turbo.errors.WebError
import dev.hotwire.core.turbo.errors.WebSslError
import dev.hotwire.core.turbo.http.HotwireHttpClient
import dev.hotwire.core.turbo.http.HttpRepository
import dev.hotwire.core.turbo.offline.*
import dev.hotwire.core.turbo.offline.OfflineHttpRepository
import dev.hotwire.core.turbo.offline.OfflinePreCacheRequest
import dev.hotwire.core.turbo.offline.OfflineRequestHandler
import dev.hotwire.core.turbo.offline.OfflineWebViewRequestInterceptor
import dev.hotwire.core.turbo.util.isHttpGetRequest
import dev.hotwire.core.turbo.util.runOnUiThread
import dev.hotwire.core.turbo.util.toJson
import dev.hotwire.core.turbo.visit.Visit
import dev.hotwire.core.turbo.visit.VisitAction
import dev.hotwire.core.turbo.visit.VisitOptions
import dev.hotwire.core.turbo.webview.HotwireWebView
import dev.hotwire.core.turbo.webview.WebViewInfo
import dev.hotwire.core.turbo.webview.WebViewVersionCompatibility
import kotlinx.coroutines.launch
import java.util.Date

Expand Down Expand Up @@ -665,13 +678,27 @@ class Session(

@SuppressLint("SetJavaScriptEnabled")
private fun initializeWebView() {
val webViewInfo = Hotwire.webViewInfo(context)
val requiredVersion = WebViewInfo.REQUIRED_WEBVIEW_VERSION

logEvent(
"WebView info",
"package" to (webView.packageName ?: ""),
"version" to (webView.versionName ?: ""),
"major version" to (webView.majorVersion ?: "")
"package" to (webViewInfo.packageInfo?.packageName ?: ""),
"type" to (webViewInfo.webViewTypeName),
"version" to (webViewInfo.majorVersion ?: "")
)

if (WebViewVersionCompatibility.isOutdated(context, requiredVersion)) {
logWarning(
"WebView outdated",
"The Chromium WebView installed on the device is outdated. Minimum version " +
"$requiredVersion is required for modern browsers in Rails 8. " +
"If you're using an emulator, ensure it has Play Services enabled and " +
"install the latest WebView version from the Play Store: " +
"${webViewInfo.playStoreWebViewAppUri}"
)
}

webView.apply {
addJavascriptInterface(this@Session, "TurboSession")
webChromeClient = WebChromeClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class WebViewInfo internal constructor(context: Context) {
UNKNOWN
}

companion object {
/**
* Rails 8 requires Chromium 120+ for "modern" browsers.
*/
const val REQUIRED_WEBVIEW_VERSION = 120
}

/**
* The system WebView's package info (corresponds to Chrome or Android System WebView).
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dev.hotwire.core.turbo.webview

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.text.Html
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.hotwire.core.R
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.turbo.webview.WebViewInfo.WebViewType

class WebViewVersionCompatibility {
companion object {
/**
* Determines whether the WebView component version installed on the device is less
* than the `requiredVersion`.
*
* @return True if the WebView is outdated, otherwise false.
*/
fun isOutdated(context: Context, requiredVersion: Int): Boolean {
val versionInfo = Hotwire.webViewInfo(context)
val majorVersion = versionInfo.majorVersion
val type = versionInfo.webViewType

return type != WebViewType.UNKNOWN &&
majorVersion != null &&
majorVersion < requiredVersion
}

/**
* Display an alert dialog if the WebView component version installed on the device is
* less than the `requiredVersion`. The user can tap "Update" to update the
* corresponding "Google Chrome" or "Android System WebView" app in the Play Store.
*
* @return True if the WebView is outdated and the dialog is displayed, otherwise false.
*/
fun displayUpdateDialogIfOutdated(activity: Activity, requiredVersion: Int): Boolean {
val versionInfo = Hotwire.webViewInfo(activity)

return if (isOutdated(activity, requiredVersion)) {
val descriptionResId = when (versionInfo.webViewType) {
WebViewType.CHROME -> R.string.webview_error_chrome_description
else -> R.string.webview_error_system_description
}

val formattedDescription = activity.getString(descriptionResId)
.format(versionInfo.majorVersion, requiredVersion)

MaterialAlertDialogBuilder(activity)
.setTitle(R.string.webview_error_title)
.setMessage(Html.fromHtml(formattedDescription, 0))
.setNegativeButton(R.string.hotwire_dialog_cancel) { dialog, _ ->
dialog.dismiss()
}
.setPositiveButton(R.string.webview_error_update) { dialog, _ ->
try {
activity.startActivity(Intent(Intent.ACTION_VIEW, versionInfo.playStoreWebViewAppUri))
} catch (_: ActivityNotFoundException) {
Toast.makeText(activity, R.string.webview_error_store_unavailable, Toast.LENGTH_LONG).show()
}
dialog.dismiss()
}
.create()
.show()

true
} else {
false
}
}
}
}
5 changes: 5 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
<string name="hotwire_file_chooser_select_multiple">Select file(s)</string>
<string name="hotwire_dialog_ok">OK</string>
<string name="hotwire_dialog_cancel">Cancel</string>
<string name="webview_error_title">Update Required</string>
<string name="webview_error_system_description">The <![CDATA[<b>Android System WebView v%1$d</b>]]> app on your device, provided by Google, is outdated. To work properly, <![CDATA[<b>v%2$d is required</b>]]> and you can update in the Play Store.</string>
<string name="webview_error_chrome_description">The <![CDATA[<b>Google Chrome v%1$d</b>]]> app on your device is outdated. To work properly, <![CDATA[<b>v%2$d is required</b>]]> and you can update in the Play Store.</string>
<string name="webview_error_store_unavailable">Play Store app is not available</string>
<string name="webview_error_update">Update</string>
</resources>
7 changes: 7 additions & 0 deletions demo/src/main/kotlin/dev/hotwire/demo/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import com.google.android.material.bottomnavigation.BottomNavigationView
import dev.hotwire.core.turbo.webview.WebViewInfo
import dev.hotwire.core.turbo.webview.WebViewVersionCompatibility
import dev.hotwire.demo.R
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.tabs.HotwireBottomNavigationController
Expand All @@ -23,6 +25,11 @@ class MainActivity : HotwireActivity() {
findViewById<View>(R.id.root).applyDefaultImeWindowInsets()

initializeBottomTabs()

WebViewVersionCompatibility.displayUpdateDialogIfOutdated(
activity = this,
requiredVersion = WebViewInfo.REQUIRED_WEBVIEW_VERSION
)
}

private fun initializeBottomTabs() {
Expand Down
2 changes: 0 additions & 2 deletions demo/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<resources>
<string name="app_name">Hotwire Native</string>
<string name="error_web_home">Error loading page</string>
<string name="error_web_home_description">The demo server may be starting up. Pull-to-refresh to try again in a minute.</string>
<string name="menu_progress">Loading…</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import dev.hotwire.core.bridge.BridgeDelegate
import dev.hotwire.core.turbo.errors.VisitError
import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_FILES
import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION
import dev.hotwire.core.turbo.errors.VisitError
import dev.hotwire.core.turbo.webview.HotwireWebChromeClient
import dev.hotwire.core.turbo.webview.HotwireWebView
import dev.hotwire.navigation.R
Expand Down Expand Up @@ -128,7 +129,9 @@ open class HotwireWebBottomSheetFragment : HotwireBottomSheetFragment(), Hotwire

@SuppressLint("InflateParams")
override fun createErrorView(error: VisitError): View {
return layoutInflater.inflate(R.layout.hotwire_error, null)
return layoutInflater.inflate(R.layout.hotwire_error, null).apply {
findViewById<TextView>(R.id.hotwire_error_description).text = error.description()
}
}

override fun createWebChromeClient(): HotwireWebChromeClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import dev.hotwire.core.bridge.BridgeDelegate
import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_FILES
Expand Down Expand Up @@ -146,7 +147,9 @@ open class HotwireWebFragment : HotwireFragment(), HotwireWebFragmentCallback {

@SuppressLint("InflateParams")
override fun createErrorView(error: VisitError): View {
return layoutInflater.inflate(R.layout.hotwire_error, null)
return layoutInflater.inflate(R.layout.hotwire_error, null).apply {
findViewById<TextView>(R.id.hotwire_error_description).text = error.description()
}
}

override fun createWebChromeClient(): HotwireWebChromeClient {
Expand Down
14 changes: 13 additions & 1 deletion navigation-fragments/src/main/res/layout/hotwire_error.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.MaterialComponents.Headline6"
android:id="@+id/hotwire_error_message"
android:id="@+id/hotwire_error_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
Expand All @@ -19,4 +19,16 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".40" />

<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.MaterialComponents.Caption"
android:id="@+id/hotwire_error_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/hotwire_error_title" />

</androidx.constraintlayout.widget.ConstraintLayout>