Skip to content
Open
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
109 changes: 108 additions & 1 deletion app/src/main/java/com/github/kr328/clash/BaseActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.kr328.clash

import android.app.ActivityManager
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
Expand All @@ -12,14 +13,19 @@ import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.bridge.ClashException
import com.github.kr328.clash.core.bridge.Bridge
import com.github.kr328.clash.design.Design
import com.github.kr328.clash.design.databinding.DesignAboutBinding
import com.github.kr328.clash.design.model.DarkMode
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.design.view.TvActivityShell
import com.github.kr328.clash.design.view.TvNavigationBar
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.util.ActivityResultLifecycle
Expand All @@ -39,14 +45,16 @@ abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
protected val uiStore by lazy { UiStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED)
protected var activityStarted: Boolean = false
private var tvShell: TvActivityShell? = null
protected val clashRunning: Boolean
get() = Remote.broadcasts.clashRunning
protected var design: D? = null
set(value) {
field = value
if (value != null) {
setContentView(value.root)
setContentView(wrapTvContent(value.root))
} else {
tvShell = null
setContentView(View(this))
}
}
Expand Down Expand Up @@ -86,6 +94,103 @@ abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
}
}

protected fun openTvTab(intent: Intent) {
if (isTelevision) {
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_NO_ANIMATION)
}
startActivity(intent)
if (isTelevision) overridePendingTransition(0, 0)
}

private val isTelevision: Boolean
get() = resources.configuration.uiMode and Configuration.UI_MODE_TYPE_MASK ==
Configuration.UI_MODE_TYPE_TELEVISION

private fun wrapTvContent(content: View): View {
val activeTab = currentTvTab() ?: return content.also { tvShell = null }
if (!isTelevision || this is MainActivity) return content.also { tvShell = null }

return TvActivityShell(this).also { shell ->
tvShell = shell
shell.setContent(content)
shell.navigationBar.apply {
setActiveTab(activeTab)
proxyEnabled = clashRunning
onTabSelected = ::openTvTab
}

if (isTopLevelTvTab()) {
content.findViewById<View>(R.id.activity_bar_close_view)?.visibility = View.GONE
}
shell.focusFirstContent()
}
}

private fun currentTvTab(): TvNavigationBar.Tab? = when (this) {
is MainActivity -> TvNavigationBar.Tab.Home
is ProxyActivity -> TvNavigationBar.Tab.Proxy
is ProfilesActivity,
is NewProfileActivity,
is PropertiesActivity,
is FilesActivity,
is ProvidersActivity -> TvNavigationBar.Tab.Profiles
is LogsActivity,
is LogcatActivity -> TvNavigationBar.Tab.Logs
is SettingsActivity,
is AppSettingsActivity,
is NetworkSettingsActivity,
is OverrideSettingsActivity,
is MetaFeatureSettingsActivity,
is AccessControlActivity -> TvNavigationBar.Tab.Settings
is HelpActivity -> TvNavigationBar.Tab.Help
else -> null
}

private fun isTopLevelTvTab(): Boolean = when (this) {
is ProxyActivity,
is ProfilesActivity,
is LogsActivity,
is SettingsActivity,
is HelpActivity -> true
else -> false
}

private fun openTvTab(tab: TvNavigationBar.Tab) {
if (tab == currentTvTab() && isTopLevelTvTab()) {
tvShell?.focusFirstContent()
return
}

when (tab) {
TvNavigationBar.Tab.Home -> openTvTab(
MainActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP),
)
TvNavigationBar.Tab.Proxy -> if (clashRunning) openTvTab(ProxyActivity::class.intent)
TvNavigationBar.Tab.Profiles -> openTvTab(ProfilesActivity::class.intent)
TvNavigationBar.Tab.Logs -> openTvTab(
if (LogcatService.running) LogcatActivity::class.intent else LogsActivity::class.intent,
)
TvNavigationBar.Tab.Settings -> openTvTab(SettingsActivity::class.intent)
TvNavigationBar.Tab.Help -> openTvTab(HelpActivity::class.intent)
TvNavigationBar.Tab.About -> launch { showTvAbout() }
}
}

private suspend fun showTvAbout() {
val versionName = withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName + "\n" +
Bridge.nativeCoreVersion().replace("_", "-")
}
withContext(Dispatchers.Main) {
val about = DesignAboutBinding.inflate(layoutInflater).apply {
this.versionName = versionName
}
androidx.appcompat.app.AlertDialog.Builder(this@BaseActivity)
.setView(about.root)
.show()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyDayNight()
Expand Down Expand Up @@ -175,10 +280,12 @@ abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
}

override fun onStarted() {
tvShell?.navigationBar?.proxyEnabled = true
events.trySend(Event.ClashStart)
}

override fun onStopped(cause: String?) {
tvShell?.navigationBar?.proxyEnabled = false
events.trySend(Event.ClashStop)

if (cause != null && activityStarted) {
Expand Down
72 changes: 62 additions & 10 deletions app/src/main/java/com/github/kr328/clash/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.core.graphics.drawable.IconCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.model.ProxySort
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.util.startClashService
Expand Down Expand Up @@ -52,29 +53,33 @@ class MainActivity : BaseActivity<MainDesign>() {
}
design.requests.onReceive {
when (it) {
MainDesign.Request.OpenHome -> {
design.fetch()
design.focusPrimaryAction()
}
MainDesign.Request.ToggleStatus -> {
if (clashRunning)
stopClashService()
else
design.startClash()
}
MainDesign.Request.OpenProxy ->
startActivity(ProxyActivity::class.intent)
openTvTab(ProxyActivity::class.intent)
MainDesign.Request.OpenProfiles ->
startActivity(ProfilesActivity::class.intent)
openTvTab(ProfilesActivity::class.intent)
MainDesign.Request.OpenProviders ->
startActivity(ProvidersActivity::class.intent)
openTvTab(ProvidersActivity::class.intent)
MainDesign.Request.OpenLogs -> {
if (LogcatService.running) {
startActivity(LogcatActivity::class.intent)
openTvTab(LogcatActivity::class.intent)
} else {
startActivity(LogsActivity::class.intent)
openTvTab(LogsActivity::class.intent)
}
}
MainDesign.Request.OpenSettings ->
startActivity(SettingsActivity::class.intent)
openTvTab(SettingsActivity::class.intent)
MainDesign.Request.OpenHelp ->
startActivity(HelpActivity::class.intent)
openTvTab(HelpActivity::class.intent)
MainDesign.Request.OpenAbout ->
design.showAbout(queryAppVersionName())
}
Expand All @@ -97,18 +102,58 @@ class MainActivity : BaseActivity<MainDesign>() {
val providers = withClash {
queryProviders()
}
val currentProxy = if (clashRunning) {
withClash {
val rootGroup = queryProxyGroupNames(true).firstOrNull()
val groupNames = queryProxyGroupNames(false).toHashSet()

rootGroup?.let { group ->
val visited = mutableSetOf(group)
var node = queryProxyGroup(group, ProxySort.Default).now
val route = mutableListOf<String>()

while (node.isNotBlank()) {
route += node
if (node !in groupNames || !visited.add(node)) break
val next = queryProxyGroup(node, ProxySort.Default).now
if (next.isBlank() || next == node) break
node = next
}

group to route.distinct().joinToString(" → ")
}
}
} else {
null
}

setMode(state.mode)
setHasProviders(providers.isNotEmpty())
setProviderCount(providers.size)
setCurrentProxy(currentProxy?.first, currentProxy?.second)
val traffic = if (clashRunning) queryTrafficStats() else TrafficSnapshot()
setForwarded(traffic.total)
setTrafficStats(traffic.now, traffic.total, traffic.connections, traffic.memory)

withProfile {
setProfileName(queryActive()?.name)
}
}

private suspend fun MainDesign.fetchTraffic() {
withClash {
setForwarded(queryTrafficTotal())
val traffic = queryTrafficStats()
setForwarded(traffic.total)
setTrafficStats(traffic.now, traffic.total, traffic.connections, traffic.memory)
}

private suspend fun queryTrafficStats(): TrafficSnapshot {
return withClash {
TrafficSnapshot(
now = queryTrafficNow(),
total = queryTrafficTotal(),
connections = queryActiveConnections(),
memory = queryMemory(),
)
}
}

Expand All @@ -118,7 +163,7 @@ class MainActivity : BaseActivity<MainDesign>() {
if (active == null || !active.imported) {
showToast(DesignR.string.no_profile_selected, ToastDuration.Long) {
setAction(DesignR.string.profiles) {
startActivity(ProfilesActivity::class.intent)
openTvTab(ProfilesActivity::class.intent)
}
}

Expand Down Expand Up @@ -165,6 +210,13 @@ class MainActivity : BaseActivity<MainDesign>() {
setupShortcuts()
}

private data class TrafficSnapshot(
val now: Long = 0L,
val total: Long = 0L,
val connections: Int = 0,
val memory: Long = 0L,
)

private fun setupShortcuts() {
// Skip dynamic shortcut setup when the app icon is hidden.
if (uiStore.hideAppIcon) return
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/cpp/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryTrafficTotal(JNIEnv *e
return (jlong) (down_scale_traffic(upload) << 32u | down_scale_traffic(download));
}

JNIEXPORT jint JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryActiveConnections(JNIEnv *env,
jobject thiz) {
TRACE_METHOD();

return (jint) queryActiveConnections();
}

JNIEXPORT jlong JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryMemory(JNIEnv *env, jobject thiz) {
TRACE_METHOD();

return (jlong) queryMemory();
}

JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeNotifyDnsChanged(JNIEnv *env, jobject thiz,
jstring dns_list) {
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/golang/native/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ func queryTotal(upload, download *C.uint64_t) {
*download = C.uint64_t(down)
}

//export queryActiveConnections
func queryActiveConnections() C.int {
return C.int(tunnel.ActiveConnections())
}

//export queryMemory
func queryMemory() C.uint64_t {
return C.uint64_t(tunnel.Memory())
}

//export queryGroupNames
func queryGroupNames(excludeNotSelectable C.int) *C.char {
return marshalJson(tunnel.QueryProxyGroupNames(excludeNotSelectable != 0))
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/golang/native/tunnel/statistic.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ func Now() (up int64, down int64) {
func Total() (up int64, down int64) {
return statistic.DefaultManager.Total()
}

func ActiveConnections() int {
count := 0
statistic.DefaultManager.Range(func(statistic.Tracker) bool {
count++
return true
})
return count
}

func Memory() uint64 {
return statistic.DefaultManager.Memory()
}
10 changes: 9 additions & 1 deletion core/src/main/java/com/github/kr328/clash/core/Clash.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ object Clash {
return Bridge.nativeQueryTrafficTotal()
}

fun queryActiveConnections(): Int {
return Bridge.nativeQueryActiveConnections()
}

fun queryMemory(): Long {
return Bridge.nativeQueryMemory()
}

fun notifyDnsChanged(dns: List<String>) {
Bridge.nativeNotifyDnsChanged(dns.toSet().joinToString(separator = ","))
}
Expand Down Expand Up @@ -257,4 +265,4 @@ object Clash {
private fun parseAgeKeyPair(value: String): AgeKeyPair {
return Json.Default.decodeFromString(AgeKeyPair.serializer(), value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ object Bridge {
external fun nativeQueryTunnelState(): String
external fun nativeQueryTrafficNow(): Long
external fun nativeQueryTrafficTotal(): Long
external fun nativeQueryActiveConnections(): Int
external fun nativeQueryMemory(): Long
external fun nativeNotifyDnsChanged(dnsList: String)
external fun nativeNotifyTimeZoneChanged(name: String, offset: Int)
external fun nativeNotifyInstalledAppChanged(uidList: String)
Expand Down Expand Up @@ -75,4 +77,4 @@ object Bridge {

nativeInit(home, versionName, sdkVersion)
}
}
}
Loading