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 @@ -13,6 +13,7 @@ import com.flowfoundation.wallet.manager.account.DeviceInfoManager
import com.flowfoundation.wallet.manager.app.AppLifecycleObserver
import com.flowfoundation.wallet.manager.app.PageLifecycleObserver
import com.flowfoundation.wallet.manager.app.refreshChainNetwork
import com.flowfoundation.wallet.manager.blocklist.BlockManager
import com.flowfoundation.wallet.manager.cadence.CadenceApiManager
import com.flowfoundation.wallet.manager.coin.CoinRateManager
import com.flowfoundation.wallet.manager.coin.CustomTokenManager
Expand Down Expand Up @@ -88,6 +89,7 @@ object LaunchManager {
private fun runWorker() {
CadenceApiManager.init()
MixpanelManager.identifyUserProfile()
BlockManager.initialize()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.flowfoundation.wallet.manager.blocklist

import android.net.Uri
import com.flowfoundation.wallet.utils.ioScope
import com.flowfoundation.wallet.utils.loge
import com.google.gson.Gson
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.net.URL
import java.util.concurrent.TimeUnit

object BlockManager {

private val TAG = BlockManager::class.java.simpleName
private const val BLOCKLIST_URL = "https://flow-blocklist.vercel.app/api/domain"
private const val CACHE_EXPIRE_TIME_HOURS = 2L

private var blockList: Set<String> = emptySet()
private var lastFetchTime: Long = 0L

private val mutex = Mutex()

fun initialize() {
ioScope {
refreshBlockListIfNeeded(forceRefresh = true)
}
}

suspend fun isBlocked(url: String): Boolean {
refreshBlockListIfNeeded()

try {
val uri = Uri.parse(url)
val host = uri.host ?: return false

return mutex.withLock {
blockList.any { blockedDomain ->
host == blockedDomain || host.matches(Regex("^[\\w.-]+\\.$blockedDomain$"))
}
}
} catch (e: Exception) {
loge(e)
return false
}
}

private suspend fun refreshBlockListIfNeeded(forceRefresh: Boolean = false) {
val needRefresh = mutex.withLock {
val currentTime = System.currentTimeMillis()
val isExpired = currentTime - lastFetchTime > TimeUnit.HOURS.toMillis(CACHE_EXPIRE_TIME_HOURS)
forceRefresh || isExpired || blockList.isEmpty()
}

if (needRefresh) {
fetchBlockList()
}
}

private fun fetchBlockList() {
ioScope {
try {
val connection = URL(BLOCKLIST_URL).openConnection()
connection.connectTimeout = 10000
connection.readTimeout = 15000
val text = connection.getInputStream().bufferedReader().use { it.readText() }
val response = Gson().fromJson(text, BlockListResponse::class.java)
val mergedList = mutableSetOf<String>()

response.flow?.let { flowList ->
mergedList.addAll(flowList)
}

response.evm?.let { evmList ->
mergedList.addAll(evmList)
}
mutex.withLock {
blockList = mergedList
lastFetchTime = System.currentTimeMillis()
loge(TAG, "Block list updated, total domains: ${blockList.size}")
}
} catch (e: Exception) {
loge(TAG, "Failed to fetch block list: ${e.message}")
}
}
}
}

data class BlockListResponse(
val flow: List<String>?,
val evm: List<String>?
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.text.Html
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.UnderlineSpan
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.View
import android.webkit.CookieManager
import android.webkit.ValueCallback
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.ColorInt
import com.flowfoundation.wallet.BuildConfig
import com.flowfoundation.wallet.R
import com.flowfoundation.wallet.manager.blocklist.BlockManager
import com.flowfoundation.wallet.manager.evm.loadInitJS
import com.flowfoundation.wallet.manager.evm.loadProviderJS
import com.flowfoundation.wallet.manager.walletconnect.WalletConnect
Expand All @@ -38,6 +47,13 @@ class LilicoWebView : WebView {
private var callback: WebviewCallback? = null
var isLoading = false

private lateinit var blockedViewLayout: View
private lateinit var tvBlockedUrl: TextView
private lateinit var tvBlockedInfo: TextView
private lateinit var tvIgnoreWarning: TextView

private var blockedUrl: String? = null

constructor(context: Context) : super(context) {
initWebView()
}
Expand All @@ -57,6 +73,7 @@ class LilicoWebView : WebView {

// Disable hardware acceleration for this WebView to prevent some layout issues
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
initBlockedViewLayout()

with(settings) {
loadsImagesAutomatically = true
Expand Down Expand Up @@ -88,6 +105,77 @@ class LilicoWebView : WebView {
}
}

private fun initBlockedViewLayout() {
post {
if (parent is ViewGroup) {
val parent = parent as ViewGroup

for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child.id == R.id.cl_blocked_view) {
blockedViewLayout = child
setupBlockedViewLayout()
return@post
}
}

blockedViewLayout = LayoutInflater.from(context).inflate(R.layout.layout_blocked_view, parent, false)

val layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
blockedViewLayout.layoutParams = layoutParams

blockedViewLayout.visibility = View.GONE

parent.addView(blockedViewLayout)

setupBlockedViewLayout()
}
}
}

private fun setupBlockedViewLayout() {
tvBlockedUrl = blockedViewLayout.findViewById(R.id.tv_blocked_url)
tvBlockedInfo = blockedViewLayout.findViewById(R.id.tv_blocked_info)
tvIgnoreWarning = blockedViewLayout.findViewById(R.id.tv_ignore_warning)

tvBlockedInfo.text = Html.fromHtml(context.getString(R.string.blocked_info), Html.FROM_HTML_MODE_LEGACY)
tvBlockedInfo.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Outblock/flow-blocklist"))
context.startActivity(intent)
}

val content = SpannableString(context.getString(R.string.ignore_warning) )
content.setSpan(UnderlineSpan(), 0, content.length, 0)
tvIgnoreWarning.text = content

tvIgnoreWarning.setOnClickListener {
hideBlockedViewLayout()
blockedUrl?.let { url ->
blockedUrl = null
loadUrl(url)
}
}
}

private fun showBlockedViewLayout(url: String) {
blockedUrl = url

val uri = Uri.parse(url)
val host = uri.host ?: url

val blockedUrlText = context.getString(R.string.blocked_url, host)
tvBlockedUrl.text = blockedUrlText

blockedViewLayout.visibility = View.VISIBLE
}

private fun hideBlockedViewLayout() {
blockedViewLayout.visibility = View.GONE
}

fun setWebViewCallback(callback: WebviewCallback?) {
this.callback = callback
}
Expand Down Expand Up @@ -191,6 +279,15 @@ class LilicoWebView : WebView {
} else if (it.toString() == "about:blank#blocked") {
return true
}
uiScope {
if (BlockManager.isBlocked(it.toString())) {
logd(TAG, "URL blocked: $url")
showBlockedViewLayout(it.toString())
loadUrl("about:blank#blocked")
isLoading = false
return@uiScope
}
}
}
return super.shouldOverrideUrlLoading(view, request)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator
import com.flowfoundation.wallet.R
import com.flowfoundation.wallet.databinding.WidgetSendButtonBinding
import com.flowfoundation.wallet.page.security.securityVerification
import com.flowfoundation.wallet.utils.extensions.res2color
import com.flowfoundation.wallet.utils.extensions.setVisible
import com.flowfoundation.wallet.utils.findActivity
import com.flowfoundation.wallet.utils.logd
Expand Down Expand Up @@ -74,6 +75,14 @@ class SendButton : TouchScaleCardView {
return true
}

fun setWarningButton(isWarning: Boolean) {
with(binding) {
setCardBackgroundColor(if (isWarning) R.color.info_error_red.res2color() else R.color.button_color.res2color())
holdToSend.setTextColor(if (isWarning) R.color.white.res2color() else R.color.brightest_text.res2color())
progressBar.setIndicatorColor(if (isWarning) R.color.white_80.res2color() else R.color.primary80.res2color())
}
}

fun updateDefaultText(textId: Int) {
defaultTextId = textId
if (textId != 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import androidx.transition.*
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.nftco.flow.sdk.hexToBytes
import com.flowfoundation.wallet.databinding.DialogFclSignMessageBinding
import com.flowfoundation.wallet.manager.blocklist.BlockManager
import com.flowfoundation.wallet.manager.evm.COALinkCheckManager
import com.flowfoundation.wallet.page.browser.loadFavicon
import com.flowfoundation.wallet.page.browser.toFavIcon
import com.flowfoundation.wallet.utils.extensions.isVisible
import com.flowfoundation.wallet.utils.extensions.res2String
import com.flowfoundation.wallet.utils.extensions.res2color
import com.flowfoundation.wallet.utils.extensions.setVisible
import com.flowfoundation.wallet.utils.extensions.visible
import com.flowfoundation.wallet.utils.ioScope
import com.flowfoundation.wallet.utils.uiScope
import com.flowfoundation.wallet.widgets.webview.fcl.model.FclDialogModel


Expand Down Expand Up @@ -62,6 +66,15 @@ class EVMSignMessageDialog : BottomSheetDialogFragment() {
}
}
scriptHeaderWrapper.setOnClickListener { toggleScriptVisible() }

uiScope {
if (data.url.isNullOrEmpty()) {
return@uiScope
}
val isBlockedUrl = BlockManager.isBlocked(data.url)
flBlockedTip.setVisible(isBlockedUrl)
actionButton.setWarningButton(isBlockedUrl)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flowfoundation.wallet.widgets.webview.evm.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
Expand All @@ -12,16 +13,20 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.fragment.app.FragmentManager
import com.flowfoundation.wallet.R
import com.flowfoundation.wallet.databinding.DialogEvmSignTypedDataBinding
import com.flowfoundation.wallet.manager.blocklist.BlockManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.flowfoundation.wallet.manager.evm.COALinkCheckManager
import com.flowfoundation.wallet.page.browser.loadFavicon
import com.flowfoundation.wallet.page.browser.toFavIcon
import com.flowfoundation.wallet.utils.ScreenUtils
import com.flowfoundation.wallet.utils.extensions.gone
import com.flowfoundation.wallet.utils.extensions.res2String
import com.flowfoundation.wallet.utils.extensions.res2color
import com.flowfoundation.wallet.utils.extensions.setVisible
import com.flowfoundation.wallet.utils.extensions.visible
import com.flowfoundation.wallet.utils.ioScope
import com.flowfoundation.wallet.utils.shortenEVMString
import com.flowfoundation.wallet.utils.uiScope
import com.flowfoundation.wallet.widgets.webview.evm.model.EVMTypedMessage
import com.flowfoundation.wallet.widgets.webview.fcl.model.FclDialogModel
import com.google.android.material.bottomsheet.BottomSheetBehavior
Expand Down Expand Up @@ -161,6 +166,14 @@ class EVMSignTypedDataDialog : BottomSheetDialogFragment() {
}
}
}
uiScope {
if (data.url.isNullOrEmpty()) {
return@uiScope
}
val isBlockedUrl = BlockManager.isBlocked(data.url)
flBlockedTip.setVisible(isBlockedUrl)
actionButton.setWarningButton(isBlockedUrl)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.flowfoundation.wallet.databinding.DialogEvmAccountBinding
import com.flowfoundation.wallet.manager.app.chainNetWorkString
import com.flowfoundation.wallet.manager.blocklist.BlockManager
import com.flowfoundation.wallet.manager.emoji.AccountEmojiManager
import com.flowfoundation.wallet.manager.emoji.model.Emoji
import com.flowfoundation.wallet.manager.evm.EVMWalletManager
import com.flowfoundation.wallet.page.browser.loadFavicon
import com.flowfoundation.wallet.page.browser.toFavIcon
import com.flowfoundation.wallet.utils.extensions.capitalizeV2
import com.flowfoundation.wallet.utils.extensions.setVisible
import com.flowfoundation.wallet.utils.extensions.urlHost
import com.flowfoundation.wallet.utils.uiScope
import com.flowfoundation.wallet.widgets.webview.evm.model.EVMDialogModel
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlin.coroutines.Continuation
Expand Down Expand Up @@ -61,6 +64,20 @@ class EvmRequestAccountDialog : BottomSheetDialogFragment() {
result?.resume(true)
dismiss()
}

uiScope {
if (data.url.isNullOrEmpty()) {
return@uiScope
}
val isBlockedUrl = BlockManager.isBlocked(data.url)
flBlockedTip.setVisible(isBlockedUrl)
btnConnect.setVisible(isBlockedUrl.not())
flBlockedConnect.setVisible(isBlockedUrl)
flBlockedConnect.setOnClickListener {
result?.resume(true)
dismiss()
}
}
}
}

Expand Down
Loading
Loading