____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* Copyright (C) 2021 by Max Lv *
* Copyright (C) 2021 by Mygod Studio *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.widget
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.doOnPreDraw
import com.google.android.material.bottomappbar.BottomAppBar
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.bg.BaseService
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.ui.MainActivity
import io.nekohasekai.sagernet.utils.FormatFileSizeCompat
import io.nekohasekai.sagernet.utils.Theme
import kotlinx.coroutines.Job
class StatsBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.bottomAppBarStyle,
) : BottomAppBar(context, attrs, defStyleAttr) {
private var testJob: Job? = null
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var behavior: YourBehavior
override fun getBehavior(): YourBehavior {
if (!this::behavior.isInitialized) behavior = YourBehavior()
return behavior
}
override fun performShow() {
super.performShow()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isNavigationBarAtBottom(this)) {
val activity = context.findActivity()!!
val insetController = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
insetController.isAppearanceLightNavigationBars =
if (DataStore.appTheme == Theme.BLACK) !Theme.usingNightMode() else false
}
}
override fun performHide() {
super.performHide()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isNavigationBarAtBottom(this)) {
val activity = context.findActivity()!!
val insetController = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
insetController.isAppearanceLightNavigationBars = !Theme.usingNightMode()
}
}
private fun isNavigationBarAtBottom(view: View): Boolean {
val insets = ViewCompat.getRootWindowInsets(view)!!
val navigationBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
return navigationBar.bottom > 0 && navigationBar.left == 0 && navigationBar.right == 0
}
class YourBehavior : Behavior() {
private fun isNavigationBarAtBottom(child: View): Boolean {
val insets = ViewCompat.getRootWindowInsets(child)!!
val navigationBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
return navigationBar.bottom > 0 && navigationBar.left == 0 && navigationBar.right == 0
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
type: Int, consumed: IntArray,
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed + dyUnconsumed,
dxUnconsumed,
0,
type,
consumed
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isNavigationBarAtBottom(child)) {
val activity = child.context.findActivity()!!
val insetController = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
when {
dyConsumed + dyUnconsumed > 0 -> {
insetController.isAppearanceLightNavigationBars = !Theme.usingNightMode()
}
dyConsumed + dyUnconsumed < 0 -> {
insetController.isAppearanceLightNavigationBars =
if (DataStore.appTheme == Theme.BLACK) !Theme.usingNightMode() else false
}
}
}
}
}
override fun setOnClickListener(l: OnClickListener?) {
statusText = findViewById(R.id.status)
txText = findViewById(R.id.tx)
rxText = findViewById(R.id.rx)
super.setOnClickListener(l)
}
private fun setStatus(text: CharSequence) {
statusText.text = text
TooltipCompat.setTooltipText(this, text)
}
fun changeState(state: BaseService.State) {
if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
doOnPreDraw {
performShow()
setStatus(context.getText(R.string.vpn_connected))
}
isEnabled = true
} else {
doOnPreDraw {
performHide()
}
testJob?.cancel()
updateTraffic(0, 0)
setStatus(
context.getText(
when (state) {
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Stopping -> R.string.stopping
else -> R.string.not_connected
}
)
)
}
}
@SuppressLint("SetTextI18n")
fun updateTraffic(txRate: Long, rxRate: Long) {
txText.text = "▲ ${
context.getString(
R.string.speed, FormatFileSizeCompat.formatFileSize(context, txRate, DataStore.useIECUnit)
)
}"
rxText.text = "▼ ${
context.getString(
R.string.speed, FormatFileSizeCompat.formatFileSize(context, rxRate, DataStore.useIECUnit)
)
}"
}
fun testConnection() {
testJob?.cancel()
val activity = context.findActivity()!!
isEnabled = false
setStatus(context.getText(R.string.connection_test_testing))
testJob = runOnDefaultDispatcher {
try {
val elapsed = activity.urlTest()
onMainDispatcher {
isEnabled = true
setStatus(
context.getString(
if (DataStore.connectionTestURL.startsWith("https://", ignoreCase = true)) {
R.string.connection_test_available
} else {
R.string.connection_test_available_http
}, elapsed
)
)
}
} catch (e: Exception) {
Logs.w(e)
onMainDispatcher {
isEnabled = true
var msg = e.localizedMessage ?: e.readableMessage
when {
msg.contains("timeout") || msg.contains("deadline") -> {
msg = context.getString(R.string.connection_test_timeout)
}
msg.contains("refused") || msg.contains("closed pipe") -> {
msg = context.getString(R.string.connection_test_refused)
}
}
setStatus(context.getString(R.string.connection_test_error, msg))
}
}
}
}
}