diff --git a/README.md b/README.md
index f48dec0..d1f42a6 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ gradle.ext.roboswag = [
'base-map',
'yandex-map',
'google-map',
+ 'webview',
'encrypted-shared-prefs'
]
diff --git a/webview/.gitignore b/webview/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/webview/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/webview/build.gradle b/webview/build.gradle
new file mode 100644
index 0000000..5b4bd46
--- /dev/null
+++ b/webview/build.gradle
@@ -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()
+}
diff --git a/webview/src/main/AndroidManifest.xml b/webview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9157adb
--- /dev/null
+++ b/webview/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseChromeWebViewClient.kt b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseChromeWebViewClient.kt
new file mode 100644
index 0000000..13754f9
--- /dev/null
+++ b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseChromeWebViewClient.kt
@@ -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
+ }
+
+}
diff --git a/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebView.kt b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebView.kt
new file mode 100644
index 0000000..8716804
--- /dev/null
+++ b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebView.kt
@@ -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) -> 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) {
+ 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
+ }
+ }
+ }
+
+}
diff --git a/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebViewClient.kt b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebViewClient.kt
new file mode 100644
index 0000000..117806b
--- /dev/null
+++ b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/BaseWebViewClient.kt
@@ -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 {
+ val cookiesMap = mutableMapOf()
+ this.split(";")
+ .forEach { cookie ->
+ val splittedCookie = cookie.trim().split("=")
+ cookiesMap[splittedCookie.first()] = splittedCookie.last()
+ }
+ return cookiesMap
+ }
+
+}
+
+enum class WebViewLoadingState {
+ LOADING,
+ ERROR,
+ LOADED
+}
diff --git a/webview/src/main/java/ru/touchin/roboswag/webview/web_view/CustomWebView.kt b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/CustomWebView.kt
new file mode 100644
index 0000000..72a4a4e
--- /dev/null
+++ b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/CustomWebView.kt
@@ -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)
+ }
+
+}
diff --git a/webview/src/main/java/ru/touchin/roboswag/webview/web_view/WebViewCallback.kt b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/WebViewCallback.kt
new file mode 100644
index 0000000..59ddc11
--- /dev/null
+++ b/webview/src/main/java/ru/touchin/roboswag/webview/web_view/WebViewCallback.kt
@@ -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)
+
+}
diff --git a/webview/src/main/res/layout/base_web_view.xml b/webview/src/main/res/layout/base_web_view.xml
new file mode 100644
index 0000000..a167e46
--- /dev/null
+++ b/webview/src/main/res/layout/base_web_view.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webview/src/main/res/values/attrs.xml b/webview/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..ff75e22
--- /dev/null
+++ b/webview/src/main/res/values/attrs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+