Merge pull request #178 from TouchInstinct/feature/base-web-view
Feature/base web view
This commit is contained in:
commit
887683f270
|
|
@ -63,6 +63,7 @@ gradle.ext.roboswag = [
|
|||
'base-map',
|
||||
'yandex-map',
|
||||
'google-map',
|
||||
'webview',
|
||||
'encrypted-shared-prefs'
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":views")
|
||||
implementation project(":kotlin-extensions")
|
||||
|
||||
implementation "com.google.android.material:material"
|
||||
implementation "androidx.constraintlayout:constraintlayout"
|
||||
implementation "androidx.core:core-ktx"
|
||||
|
||||
constraints {
|
||||
implementation("com.google.android.material:material") {
|
||||
version {
|
||||
require '1.0.0'
|
||||
}
|
||||
}
|
||||
implementation("androidx.constraintlayout:constraintlayout") {
|
||||
version {
|
||||
require '2.0.0-beta4'
|
||||
}
|
||||
}
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require '1.3.1'
|
||||
}
|
||||
}
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") {
|
||||
version {
|
||||
require '1.3.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="ru.touchin.roboswag.webview"/>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package ru.touchin.roboswag.webview.web_view
|
||||
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.JsPromptResult
|
||||
import android.webkit.JsResult
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
|
||||
open class BaseChromeWebViewClient(
|
||||
private val onJsConfirm: (() -> Unit)? = null,
|
||||
private val onJsAlert: (() -> Unit)? = null,
|
||||
private val onJsPrompt: ((String?) -> Unit)? = null,
|
||||
private val onJsError: ((error: ConsoleMessage) -> Unit)? = null
|
||||
) : WebChromeClient() {
|
||||
|
||||
override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
|
||||
onJsConfirm?.invoke()
|
||||
result?.confirm()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
|
||||
onJsAlert?.invoke()
|
||||
result?.confirm()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
if (consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
|
||||
onJsError?.invoke(consoleMessage)
|
||||
}
|
||||
return super.onConsoleMessage(consoleMessage)
|
||||
}
|
||||
|
||||
override fun onJsPrompt(view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean {
|
||||
onJsPrompt?.invoke(defaultValue)
|
||||
result?.confirm()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package ru.touchin.roboswag.webview.web_view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import ru.touchin.extensions.setOnRippleClickListener
|
||||
import ru.touchin.roboswag.views.widget.Switcher
|
||||
import ru.touchin.roboswag.webview.R
|
||||
import ru.touchin.roboswag.webview.databinding.BaseWebViewBinding
|
||||
|
||||
open class BaseWebView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int = 0
|
||||
) : Switcher(context, attrs), WebViewCallback {
|
||||
|
||||
private val binding = BaseWebViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
var onWebViewLoaded: (() -> Unit)? = null
|
||||
var onWebViewRepeatButtonClicked: (() -> Unit)? = null
|
||||
var onWebViewScrolled: ((WebView, Int, Int) -> Unit)? = null
|
||||
var onCookieLoaded: ((cookies: Map<String, String>) -> Unit)? = null
|
||||
|
||||
var onJsConfirm: (() -> Unit)? = null
|
||||
var onJsAlert: (() -> Unit)? = null
|
||||
var onJsPrompt: ((defaultValue: String?) -> Unit)? = null
|
||||
var onJsError: ((error: ConsoleMessage) -> Unit)? = null
|
||||
|
||||
var isPullToRefreshEnable = false
|
||||
set(value) {
|
||||
binding.pullToRefresh.isEnabled = value
|
||||
binding.pullToRefresh.isRefreshing = false
|
||||
field = value
|
||||
}
|
||||
|
||||
var isRedirectEnable = false
|
||||
|
||||
init {
|
||||
binding.pullToRefresh.isEnabled = isPullToRefreshEnable
|
||||
binding.apply {
|
||||
context.withStyledAttributes(attrs, R.styleable.BaseWebView, defStyleAttr, 0) {
|
||||
if (hasValue(R.styleable.BaseWebView_errorTextAppearance)) {
|
||||
TextViewCompat.setTextAppearance(errorText, getResourceId(R.styleable.BaseWebView_errorTextAppearance, 0))
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_repeatButtonTextAppearance)) {
|
||||
TextViewCompat.setTextAppearance(errorRepeatButton, getResourceId(R.styleable.BaseWebView_repeatButtonTextAppearance, 0))
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_progressBarTintColor)) {
|
||||
progressBar.indeterminateTintList = ColorStateList.valueOf(getColor(R.styleable.BaseWebView_progressBarTintColor, Color.BLACK))
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_repeatButtonBackground)) {
|
||||
errorRepeatButton.setBackgroundResource(getResourceId(R.styleable.BaseWebView_repeatButtonBackground, 0))
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_screenBackground)) {
|
||||
setBackgroundResource(getResourceId(R.styleable.BaseWebView_screenBackground, 0))
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_errorText)) {
|
||||
errorText.text = getString(R.styleable.BaseWebView_errorText)
|
||||
}
|
||||
if (hasValue(R.styleable.BaseWebView_repeatButtonText)) {
|
||||
errorRepeatButton.text = getString(R.styleable.BaseWebView_repeatButtonText)
|
||||
}
|
||||
}
|
||||
pullToRefresh.setOnRefreshListener {
|
||||
webView.reload()
|
||||
}
|
||||
errorRepeatButton.setOnRippleClickListener {
|
||||
onWebViewRepeatButtonClicked?.invoke()
|
||||
}
|
||||
webView.onScrollChanged = { scrollX, scrollY, _, _ ->
|
||||
onWebViewScrolled?.invoke(binding.webView, scrollX, scrollY)
|
||||
}
|
||||
setWebViewPreferences()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(newState: WebViewLoadingState) {
|
||||
when {
|
||||
newState == WebViewLoadingState.LOADED -> {
|
||||
onWebViewLoaded?.invoke()
|
||||
binding.pullToRefresh.isRefreshing = false
|
||||
showChild(R.id.pull_to_refresh)
|
||||
}
|
||||
newState == WebViewLoadingState.LOADING
|
||||
&& !binding.pullToRefresh.isRefreshing -> {
|
||||
showChild(R.id.progress_bar)
|
||||
}
|
||||
newState == WebViewLoadingState.ERROR -> {
|
||||
showChild(R.id.error_layout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOverrideUrlLoading(url: String?): Boolean = isRedirectEnable
|
||||
|
||||
override fun onPageCookiesLoaded(cookies: Map<String, String>) {
|
||||
onCookieLoaded?.invoke(cookies)
|
||||
}
|
||||
|
||||
fun setBaseWebViewClient(isSSlPinningEnable: Boolean = false) {
|
||||
binding.webView.webViewClient = BaseWebViewClient(this, isSSlPinningEnable)
|
||||
binding.webView.webChromeClient = BaseChromeWebViewClient(onJsConfirm, onJsAlert, onJsPrompt, onJsError)
|
||||
}
|
||||
|
||||
fun getWebView() = binding.webView
|
||||
|
||||
fun loadUrl(url: String?) {
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
|
||||
fun setState(newState: WebViewLoadingState) {
|
||||
onStateChanged(newState)
|
||||
}
|
||||
|
||||
fun setOnWebViewDisplayedContentAction(action: () -> Unit) {
|
||||
binding.webView.onWebViewDisplayedContent = action
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
open fun setWebViewPreferences() {
|
||||
binding.webView.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
|
||||
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
with(settings) {
|
||||
loadsImagesAutomatically = true
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
loadWithOverviewMode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package ru.touchin.roboswag.webview.web_view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.os.postDelayed
|
||||
|
||||
open class BaseWebViewClient(private val callback: WebViewCallback, private val isSslPinningEnable: Boolean) : WebViewClient() {
|
||||
|
||||
companion object {
|
||||
private const val WEB_VIEW_TIMEOUT_MS = 30 * 1000L // 30 sec
|
||||
}
|
||||
|
||||
private var isError = false
|
||||
private var isTimeout = true
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
isError = false
|
||||
callback.onStateChanged(WebViewLoadingState.LOADING)
|
||||
|
||||
Looper.myLooper()?.let { looper ->
|
||||
val handler = Handler(looper)
|
||||
handler.postDelayed(WEB_VIEW_TIMEOUT_MS) {
|
||||
if (isTimeout) {
|
||||
isError = true
|
||||
pageFinished()
|
||||
}
|
||||
isTimeout = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
isTimeout = false
|
||||
callback.onPageCookiesLoaded(CookieManager.getInstance().getCookie(url).processCookies())
|
||||
pageFinished()
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||
return !callback.onOverrideUrlLoading(url) && view.originalUrl != null
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
|
||||
if (isSslPinningEnable) {
|
||||
super.onReceivedSslError(view, handler, error)
|
||||
isError = true
|
||||
callback.onStateChanged(WebViewLoadingState.ERROR)
|
||||
} else {
|
||||
handler.proceed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
||||
if (!(error.errorCode == -10 && "about:blank" == request.url.toString())) {
|
||||
isError = true
|
||||
}
|
||||
pageFinished()
|
||||
}
|
||||
|
||||
private fun pageFinished() {
|
||||
callback.onStateChanged(if (isError) WebViewLoadingState.ERROR else WebViewLoadingState.LOADED)
|
||||
}
|
||||
|
||||
private fun String.processCookies(): Map<String, String> {
|
||||
val cookiesMap = mutableMapOf<String, String>()
|
||||
this.split(";")
|
||||
.forEach { cookie ->
|
||||
val splittedCookie = cookie.trim().split("=")
|
||||
cookiesMap[splittedCookie.first()] = splittedCookie.last()
|
||||
}
|
||||
return cookiesMap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class WebViewLoadingState {
|
||||
LOADING,
|
||||
ERROR,
|
||||
LOADED
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ru.touchin.roboswag.webview.web_view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.webkit.WebView
|
||||
|
||||
class CustomWebView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int = 0
|
||||
) : WebView(context, attrs, defStyleAttr) {
|
||||
|
||||
var onWebViewDisplayedContent: (() -> Unit)? = null
|
||||
var onScrollChanged: ((Int, Int, Int, Int) -> Unit)? = null
|
||||
|
||||
// https://stackoverflow.com/a/14678910
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
|
||||
if (contentHeight > 0) {
|
||||
onWebViewDisplayedContent?.invoke()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onScrollChanged(scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
|
||||
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY)
|
||||
onScrollChanged?.invoke(scrollX, scrollY, oldScrollX, oldScrollY)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.roboswag.webview.web_view
|
||||
|
||||
interface WebViewCallback {
|
||||
|
||||
fun onStateChanged(newState: WebViewLoadingState)
|
||||
|
||||
fun onOverrideUrlLoading(url: String?): Boolean
|
||||
|
||||
fun onPageCookiesLoaded(cookies: Map<String, String>)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/pull_to_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ru.touchin.roboswag.webview.web_view.CustomWebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/error_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@android:color/black"
|
||||
tools:text="Error text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_repeat_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="20dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
tools:text="Повторить" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="BaseWebView">
|
||||
<attr name="screenBackground" format="reference"/>
|
||||
<attr name="errorText" format="string"/>
|
||||
<attr name="repeatButtonText" format="string"/>
|
||||
<attr name="repeatButtonBackground" format="reference"/>
|
||||
<attr name="repeatButtonTextAppearance" format="reference"/>
|
||||
<attr name="errorTextAppearance" format="reference"/>
|
||||
<attr name="progressBarTintColor" format="color"/>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue