/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai
*
* *
* 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.ui
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.AppStats
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutTrafficItemBinding
import io.nekohasekai.sagernet.databinding.LayoutTrafficListBinding
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.utils.FormatFileSizeCompat
import io.nekohasekai.sagernet.utils.PackageCache
class ActiveFragment : Fragment(R.layout.layout_traffic_list) {
lateinit var binding: LayoutTrafficListBinding
lateinit var adapter: ActiveAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ViewCompat.setOnApplyWindowInsetsListener(view.findViewById(R.id.holder)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
bottom = bars.bottom + dp2px(64),
)
insets
}
ViewCompat.setOnApplyWindowInsetsListener(view.findViewById(R.id.traffic_list)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = bars.left + dp2px(4),
right = bars.right + dp2px(4),
bottom = bars.bottom + dp2px(64),
)
insets
}
binding = LayoutTrafficListBinding.bind(view)
adapter = ActiveAdapter()
binding.trafficList.layoutManager = FixedLinearLayoutManager(binding.trafficList)
binding.trafficList.adapter = adapter
(parentFragment as TrafficFragment).listeners.add(::emitStats)
emitStats(emptyList())
}
fun emitStats(statsList: List) {
if (statsList.isEmpty()) {
runOnMainDispatcher {
binding.holder.isVisible = true
binding.trafficList.isVisible = false
if (!SagerNet.started || DataStore.serviceMode != Key.MODE_VPN) {
binding.holder.text = getString(R.string.traffic_holder)
} else if ((activity as MainActivity).connection.service?.trafficStatsEnabled != true) {
binding.holder.text = getString(R.string.app_statistics_disabled)
} else {
binding.holder.text = getString(R.string.no_statistics)
}
}
binding.trafficList.post {
adapter.data = emptyList()
adapter.notifyDataSetChanged()
}
} else {
runOnMainDispatcher {
binding.holder.isVisible = false
binding.trafficList.isVisible = true
}
val now = System.currentTimeMillis() / 1000
val list = statsList.filter { it.deactivateAt == 0 || now - it.deactivateAt < 5 }
.toSortedSet { a, b ->
val dataA = a.uplink + a.downlink
val dataB = b.uplink + b.downlink
if (dataA != dataB) {
dataB.compareTo(dataA)
} else {
val connA = a.tcpConnections + a.udpConnections
val connB = b.tcpConnections + b.udpConnections
if (connA != connB) {
connB.compareTo(connA)
} else {
b.uid.compareTo(a.uid)
}
}
}
.toList()
binding.trafficList.post {
adapter.data = list
adapter.notifyDataSetChanged()
}
}
}
inner class ActiveAdapter : RecyclerView.Adapter() {
init {
setHasStableIds(true)
}
lateinit var data: List
override fun getItemId(position: Int): Long {
return data[position].uid.toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActiveViewHolder {
return ActiveViewHolder(
LayoutTrafficItemBinding.inflate(layoutInflater, parent, false)
)
}
override fun onBindViewHolder(holder: ActiveViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount(): Int {
if (!::data.isInitialized) return 0
return data.size
}
}
inner class ActiveViewHolder(val binding: LayoutTrafficItemBinding) : RecyclerView.ViewHolder(
binding.root
) {
fun bind(stats: AppStats) {
PackageCache.awaitLoadSync()
val packageName = when (stats.uid) {
1000 -> "android"
else -> PackageCache.uidMap[stats.uid]?.iterator()?.next()
}
binding.menu.setOnClickListener {
val popup = PopupMenu(requireContext(), it)
popup.menuInflater.inflate(R.menu.traffic_item_menu, popup.menu)
popup.setOnMenuItemClickListener(
(requireParentFragment() as TrafficFragment).ItemMenuListener(stats, packageName)
)
popup.show()
}
binding.label.text = packageName?.let {
PackageCache.loadLabel(it)
} ?: "UID ${stats.uid}"
binding.desc.text = packageName?.let {
"$packageName (${stats.uid})"
} ?: "UID ${stats.uid} (${stats.uid})"
binding.tcpConnections.text = resources.getQuantityString(R.plurals.tcp_connections, stats.tcpConnections, stats.tcpConnections)
binding.udpConnections.text = resources.getQuantityString(R.plurals.udp_connections, stats.udpConnections, stats.udpConnections)
binding.trafficUplink.text = getString(
R.string.traffic_uplink,
FormatFileSizeCompat.formatFileSize(requireContext(), stats.uplinkTotal, DataStore.useIECUnit),
FormatFileSizeCompat.formatFileSize(requireContext(), stats.uplink, DataStore.useIECUnit)
)
binding.trafficDownlink.text = getString(
R.string.traffic_downlink,
FormatFileSizeCompat.formatFileSize(requireContext(), stats.downlinkTotal, DataStore.useIECUnit),
FormatFileSizeCompat.formatFileSize(requireContext(), stats.downlink, DataStore.useIECUnit)
)
val info = packageName?.let {
PackageCache.installedApps[packageName]
} ?: PackageCache.installedApps["android"]
if (info != null) runOnDefaultDispatcher {
try {
val icon = info.loadIcon(app.packageManager)
onMainDispatcher {
binding.icon.setImageDrawable(icon)
}
} catch (_: Exception) {
}
}
}
}
}