Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa4432584e | |||
| bb1c3d915f | |||
| d8983a4761 | |||
| 9815041c77 | |||
| 41b0983582 | |||
| bb1f677d45 | |||
| de1c8bcfcc | |||
| ca6e963575 | |||
| bfb6813dc7 |
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 704
|
versionCode = 705
|
||||||
versionName = "2.0.4"
|
versionName = "2.0.5"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
if (uri.userInfo.isNullOrEmpty()) return null
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ object SocksFmt : FmtBase() {
|
|||||||
if (uri.idnHost.isEmpty()) return null
|
if (uri.idnHost.isEmpty()) return null
|
||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ object VmessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ object WireguardFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|||||||
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.Language
|
import com.v2ray.ang.dto.Language
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
@@ -386,6 +387,15 @@ object SettingsManager {
|
|||||||
return MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true)
|
return MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if VPN mode is enabled.
|
||||||
|
* @return True if VPN mode is enabled, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isVpnMode(): Boolean {
|
||||||
|
val mode = MmkvManager.decodeSettingsString(AppConfig.PREF_MODE)
|
||||||
|
return mode == null || mode == VPN
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure default settings are present in MMKV.
|
* Ensure default settings are present in MMKV.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ object V2RayServiceManager {
|
|||||||
} else {
|
} else {
|
||||||
context.toast(R.string.toast_services_start)
|
context.toast(R.string.toast_services_start)
|
||||||
}
|
}
|
||||||
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
|
val intent = if (SettingsManager.isVpnMode()) {
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||||
} else {
|
} else {
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ object V2rayConfigManager {
|
|||||||
private fun getV2rayCustomConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
|
private fun getV2rayCustomConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
|
||||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
||||||
val result = ConfigResult(true, guid, raw)
|
val result = ConfigResult(true, guid, raw)
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if (!needTun()) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,18 +334,18 @@ object V2rayConfigManager {
|
|||||||
*/
|
*/
|
||||||
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
||||||
var assets = ""
|
var assets = ""
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if (needTun()) {
|
||||||
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
initConfigCache = assets
|
|
||||||
} else {
|
|
||||||
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
||||||
if (TextUtils.isEmpty(assets)) {
|
if (TextUtils.isEmpty(assets)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
initConfigCacheWithTun = assets
|
initConfigCacheWithTun = assets
|
||||||
|
} else {
|
||||||
|
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
initConfigCache = assets
|
||||||
}
|
}
|
||||||
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
||||||
return config
|
return config
|
||||||
@@ -357,6 +357,10 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
//region some sub function
|
//region some sub function
|
||||||
|
|
||||||
|
private fun needTun(): Boolean {
|
||||||
|
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the inbound settings for V2ray.
|
* Configures the inbound settings for V2ray.
|
||||||
*
|
*
|
||||||
@@ -395,7 +399,7 @@ object V2rayConfigManager {
|
|||||||
v2rayConfig.inbounds.add(inbound2)
|
v2rayConfig.inbounds.add(inbound2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SettingsManager.isUsingHevTun()) {
|
if (needTun()) {
|
||||||
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
||||||
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
||||||
inboundTun?.sniffing = inbound1.sniffing
|
inboundTun?.sniffing = inbound1.sniffing
|
||||||
@@ -520,23 +524,25 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if(SettingsManager.isVpnMode()) {
|
||||||
//hev-socks5-tunnel dns routing
|
if (SettingsManager.isUsingHevTun()) {
|
||||||
v2rayConfig.routing.rules.add(
|
//hev-socks5-tunnel dns routing
|
||||||
0, RulesBean(
|
v2rayConfig.routing.rules.add(
|
||||||
inboundTag = arrayListOf("socks"),
|
0, RulesBean(
|
||||||
outboundTag = "dns-out",
|
inboundTag = arrayListOf("socks"),
|
||||||
port = "53",
|
outboundTag = "dns-out",
|
||||||
|
port = "53",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
v2rayConfig.routing.rules.add(
|
||||||
v2rayConfig.routing.rules.add(
|
0, RulesBean(
|
||||||
0, RulesBean(
|
inboundTag = arrayListOf("tun"),
|
||||||
inboundTag = arrayListOf("tun"),
|
outboundTag = "dns-out",
|
||||||
outboundTag = "dns-out",
|
port = "53",
|
||||||
port = "53",
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS outbound
|
// DNS outbound
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ interface BaseAdapterListener {
|
|||||||
/**
|
/**
|
||||||
* Request the host to edit the specified item.
|
* Request the host to edit the specified item.
|
||||||
* @param guid Unique identifier (GUID) of the item
|
* @param guid Unique identifier (GUID) of the item
|
||||||
|
* @param position Current position in the adapter (optional; host should validate it)
|
||||||
*/
|
*/
|
||||||
fun onEdit(guid: String)
|
fun onEdit(guid: String, position: Int)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import com.v2ray.ang.dto.GroupMapItem
|
|||||||
class GroupPagerAdapter(activity: FragmentActivity, var groups: List<GroupMapItem>) : FragmentStateAdapter(activity) {
|
class GroupPagerAdapter(activity: FragmentActivity, var groups: List<GroupMapItem>) : FragmentStateAdapter(activity) {
|
||||||
override fun getItemCount(): Int = groups.size
|
override fun getItemCount(): Int = groups.size
|
||||||
override fun createFragment(position: Int) = GroupServerFragment.newInstance(groups[position].id)
|
override fun createFragment(position: Int) = GroupServerFragment.newInstance(groups[position].id)
|
||||||
|
override fun getItemId(position: Int): Long = groups[position].id.hashCode().toLong()
|
||||||
|
override fun containsItem(itemId: Long): Boolean = groups.any { it.id.hashCode().toLong() == itemId }
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun update(groups: List<GroupMapItem>) {
|
fun update(groups: List<GroupMapItem>) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -9,7 +8,6 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.TAG
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -51,14 +49,14 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
if (mainViewModel.subscriptionId != subId) {
|
if (mainViewModel.subscriptionId != subId) {
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||||
adapter.setData(mainViewModel.serversCache, index)
|
adapter.setData(mainViewModel.serversCache, index)
|
||||||
}
|
}
|
||||||
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
||||||
adapter.isRunning = isRunning
|
adapter.isRunning = isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -2,37 +2,34 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
|
private val viewModel: LogcatViewModel by viewModels()
|
||||||
private var logsetsAll: MutableList<String> = mutableListOf()
|
private lateinit var adapter: LogcatRecyclerAdapter
|
||||||
var logsets: MutableList<String> = mutableListOf()
|
|
||||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_logcat))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_logcat))
|
||||||
|
|
||||||
|
adapter = LogcatRecyclerAdapter(viewModel, ::onLogLongClick)
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
@@ -40,58 +37,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
|
|
||||||
binding.refreshLayout.setOnRefreshListener(this)
|
binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
logsets.add(getString(R.string.pull_down_to_refresh))
|
toast(getString(R.string.pull_down_to_refresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLogcat() {
|
private fun onLogLongClick(log: String): Boolean {
|
||||||
|
Utils.setClipboard(this, log)
|
||||||
try {
|
return true
|
||||||
binding.refreshLayout.isRefreshing = true
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-d")
|
|
||||||
lst.add("-v")
|
|
||||||
lst.add("time")
|
|
||||||
lst.add("-s")
|
|
||||||
lst.add("GoLog,${ANG_PACKAGE},AndroidRuntime,System.err")
|
|
||||||
val process = withContext(Dispatchers.IO) {
|
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll = allText.toMutableList()
|
|
||||||
logsets = allText.toMutableList()
|
|
||||||
refreshData()
|
|
||||||
binding.refreshLayout.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearLogcat() {
|
|
||||||
try {
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-c")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
process.waitFor()
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll.clear()
|
|
||||||
logsets.clear()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -104,12 +55,14 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
filterLogs(newText)
|
viewModel.filter(newText)
|
||||||
|
refreshData()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
searchView.setOnCloseListener {
|
searchView.setOnCloseListener {
|
||||||
filterLogs("")
|
viewModel.filter("")
|
||||||
|
refreshData()
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,33 +72,33 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.copy_all -> {
|
R.id.copy_all -> {
|
||||||
Utils.setClipboard(this, logsets.joinToString("\n"))
|
val all = viewModel.getAll().joinToString("\n")
|
||||||
|
Utils.setClipboard(this, all)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
clearLogcat()
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
viewModel.clearLogcat()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterLogs(content: String?): Boolean {
|
|
||||||
val key = content?.trim()
|
|
||||||
logsets = if (key.isNullOrEmpty()) {
|
|
||||||
logsetsAll.toMutableList()
|
|
||||||
} else {
|
|
||||||
logsetsAll.filter { it.contains(key) }.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshData()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
getLogcat()
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
viewModel.loadLogcat()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||||
|
|
||||||
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
class LogcatRecyclerAdapter(
|
||||||
private var mActivity: LogcatActivity = activity
|
private val viewModel: LogcatViewModel,
|
||||||
|
private val onLongClick: ((String) -> Boolean)? = null
|
||||||
|
) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||||
|
|
||||||
|
override fun getItemCount() = viewModel.getAll().size
|
||||||
override fun getItemCount() = mActivity.logsets.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
try {
|
try {
|
||||||
val log = mActivity.logsets[position]
|
val logs = viewModel.getAll()
|
||||||
|
val log = logs[position]
|
||||||
|
|
||||||
if (log.isEmpty()) {
|
if (log.isEmpty()) {
|
||||||
holder.itemSubSettingBinding.logTag.text = ""
|
holder.itemSubSettingBinding.logTag.text = ""
|
||||||
holder.itemSubSettingBinding.logContent.text = ""
|
holder.itemSubSettingBinding.logContent.text = ""
|
||||||
@@ -27,8 +30,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener {
|
holder.itemView.setOnLongClickListener {
|
||||||
Utils.setClipboard(mActivity, log)
|
onLongClick?.invoke(log) ?: false
|
||||||
true
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
@@ -34,6 +33,7 @@ import com.v2ray.ang.extension.toastError
|
|||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsChangeManager
|
import com.v2ray.ang.handler.SettingsChangeManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
@@ -188,7 +188,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
V2RayServiceManager.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
} else if (SettingsManager.isVpnMode()) {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class RoutingSettingActivity : BaseActivity() {
|
class RoutingSettingActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
||||||
|
private val ownerActivity: RoutingSettingActivity
|
||||||
|
get() = this
|
||||||
private val viewModel: RoutingSettingsViewModel by viewModels()
|
private val viewModel: RoutingSettingsViewModel by viewModels()
|
||||||
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
@@ -57,7 +58,7 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
//setContentView(binding.root)
|
//setContentView(binding.root)
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_title))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_title))
|
||||||
|
|
||||||
adapter = RoutingSettingRecyclerAdapter(this, viewModel)
|
adapter = RoutingSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
@@ -202,4 +203,23 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
viewModel.reload()
|
viewModel.reload()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
|
override fun onEdit(guid: String, position: Int) {
|
||||||
|
startActivity(
|
||||||
|
Intent(ownerActivity, RoutingEditActivity::class.java)
|
||||||
|
.putExtra("position", position)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemove(guid: String, position: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare(url: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefreshData() {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,12 +12,11 @@ import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
|||||||
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
||||||
|
|
||||||
class RoutingSettingRecyclerAdapter(
|
class RoutingSettingRecyclerAdapter(
|
||||||
val activity: RoutingSettingActivity,
|
private val viewModel: RoutingSettingsViewModel,
|
||||||
private val viewModel: RoutingSettingsViewModel
|
private val adapterListener: BaseAdapterListener?
|
||||||
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
||||||
ItemTouchHelperAdapter {
|
ItemTouchHelperAdapter {
|
||||||
|
|
||||||
private var mActivity: RoutingSettingActivity = activity
|
|
||||||
override fun getItemCount() = viewModel.getAll().size
|
override fun getItemCount() = viewModel.getAll().size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
@@ -33,10 +31,7 @@ class RoutingSettingRecyclerAdapter(
|
|||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||||
mActivity.startActivity(
|
adapterListener?.onEdit("", position)
|
||||||
Intent(mActivity, RoutingEditActivity::class.java)
|
|
||||||
.putExtra("position", position)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
@@ -76,7 +71,7 @@ class RoutingSettingRecyclerAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
override fun onItemMoveCompleted() {
|
||||||
mActivity.refreshData()
|
adapterListener?.onRefreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
|
|
||||||
|
private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
|
||||||
|
private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
||||||
|
private val useHevTun by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_USE_HEV_TUNNEL) }
|
||||||
|
|
||||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||||
// Use MMKV as the storage backend for all Preferences
|
// Use MMKV as the storage backend for all Preferences
|
||||||
// This prevents inconsistencies between SharedPreferences and MMKV
|
// This prevents inconsistencies between SharedPreferences and MMKV
|
||||||
@@ -93,12 +97,21 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
mode?.setOnPreferenceChangeListener { pref, newValue ->
|
||||||
updateMode(newValue.toString())
|
val valueStr = newValue.toString()
|
||||||
|
(pref as? ListPreference)?.let { lp ->
|
||||||
|
val idx = lp.findIndexOfValue(valueStr)
|
||||||
|
lp.summary = if (idx >= 0) lp.entries[idx] else valueStr
|
||||||
|
}
|
||||||
|
updateMode(valueStr)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||||
|
|
||||||
|
useHevTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateHevTunSettings(newValue as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPreferenceSummaries() {
|
private fun initPreferenceSummaries() {
|
||||||
@@ -141,6 +154,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
updateHevTunSettings(MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true))
|
||||||
|
|
||||||
// Initialize mode-dependent UI states
|
// Initialize mode-dependent UI states
|
||||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||||
|
|
||||||
@@ -152,11 +167,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
// Initialize auto-update interval state
|
// Initialize auto-update interval state
|
||||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMode(mode: String?) {
|
private fun updateMode(value: String?) {
|
||||||
val vpn = mode == VPN
|
val vpn = value == VPN
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
fakeDns?.isEnabled = vpn
|
fakeDns?.isEnabled = vpn
|
||||||
appendHttpProxy?.isEnabled = vpn
|
appendHttpProxy?.isEnabled = vpn
|
||||||
@@ -165,6 +179,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
vpnBypassLan?.isEnabled = vpn
|
vpnBypassLan?.isEnabled = vpn
|
||||||
vpnInterfaceAddress?.isEnabled = vpn
|
vpnInterfaceAddress?.isEnabled = vpn
|
||||||
vpnMtu?.isEnabled = vpn
|
vpnMtu?.isEnabled = vpn
|
||||||
|
useHevTun?.isEnabled = vpn
|
||||||
|
updateHevTunSettings(false)
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(
|
updateLocalDns(
|
||||||
MmkvManager.decodeSettingsBool(
|
MmkvManager.decodeSettingsBool(
|
||||||
@@ -172,6 +188,12 @@ class SettingsActivity : BaseActivity() {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
updateHevTunSettings(
|
||||||
|
MmkvManager.decodeSettingsBool(
|
||||||
|
AppConfig.PREF_USE_HEV_TUNNEL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +257,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentLength?.isEnabled = enabled
|
fragmentLength?.isEnabled = enabled
|
||||||
fragmentInterval?.isEnabled = enabled
|
fragmentInterval?.isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateHevTunSettings(enabled: Boolean) {
|
||||||
|
hevTunLogLevel?.isEnabled = enabled
|
||||||
|
hevTunRwTimeout?.isEnabled = enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private inner class ActivityAdapterListener : BaseAdapterListener {
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
override fun onEdit(guid: String) {
|
override fun onEdit(guid: String, position: Int) {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(ownerActivity, SubEditActivity::class.java)
|
Intent(ownerActivity, SubEditActivity::class.java)
|
||||||
.putExtra("subId", guid)
|
.putExtra("subId", guid)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
adapterListener?.onEdit(subId)
|
adapterListener?.onEdit(subId, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
|
|||||||
@@ -9,44 +9,36 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.concatUrl
|
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
||||||
|
private val ownerActivity: UserAssetActivity
|
||||||
|
get() = this
|
||||||
|
private val viewModel: UserAssetViewModel by viewModels()
|
||||||
|
private lateinit var adapter: UserAssetAdapter
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
|
|
||||||
private val requestStoragePermissionLauncher = registerForActivityResult(
|
private val requestStoragePermissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
@@ -83,13 +75,13 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting))
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
adapter = UserAssetAdapter(viewModel, extDir, ActivityAdapterListener())
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
||||||
binding.layoutGeoFilesSources.setOnClickListener {
|
binding.layoutGeoFilesSources.setOnClickListener {
|
||||||
@@ -219,81 +211,25 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
private fun downloadGeoFiles() {
|
||||||
|
refreshData()
|
||||||
showLoading()
|
showLoading()
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
|
|
||||||
var resultCount = 0
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
assets.forEach {
|
val result = viewModel.downloadGeoFiles(extDir, httpPort)
|
||||||
try {
|
|
||||||
var result = downloadGeo(it.second, 15000, httpPort)
|
|
||||||
if (!result) {
|
|
||||||
result = downloadGeo(it.second, 15000, 0)
|
|
||||||
}
|
|
||||||
if (result)
|
|
||||||
resultCount++
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (resultCount > 0) {
|
if (result.successCount > 0) {
|
||||||
toast(getString(R.string.title_update_config_count, resultCount))
|
toast(getString(R.string.title_update_config_count, result.successCount))
|
||||||
refreshData()
|
|
||||||
} else {
|
} else {
|
||||||
toast(getString(R.string.toast_failure))
|
toast(getString(R.string.toast_failure))
|
||||||
}
|
}
|
||||||
|
refreshData()
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
|
||||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
|
||||||
val target = File(extDir, item.remarks)
|
|
||||||
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
|
|
||||||
|
|
||||||
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
|
||||||
try {
|
|
||||||
val inputStream = conn.inputStream
|
|
||||||
val responseCode = conn.responseCode
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
|
||||||
FileOutputStream(targetTemp).use { output ->
|
|
||||||
inputStream.copyTo(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetTemp.renameTo(target)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
|
||||||
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
|
||||||
builtInGeoFiles
|
|
||||||
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
|
||||||
.forEach {
|
|
||||||
list.add(
|
|
||||||
Utils.getUuid() to AssetUrlItem(
|
|
||||||
it,
|
|
||||||
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
|
||||||
locked = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list + assets
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initAssets() {
|
fun initAssets() {
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||||
@@ -305,72 +241,41 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
viewModel.reload(getGeoFilesSources())
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
override fun onEdit(guid: String, position: Int) {
|
||||||
return UserAssetViewHolder(
|
startActivity(
|
||||||
ItemRecyclerUserAssetBinding.inflate(
|
Intent(ownerActivity, UserAssetUrlActivity::class.java)
|
||||||
LayoutInflater.from(parent.context),
|
.putExtra("assetId", guid)
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
override fun onRemove(guid: String, position: Int) {
|
||||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
val asset = viewModel.getAsset(position)?.takeIf { it.first == guid }
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
?: viewModel.getAssets().find { it.first == guid }
|
||||||
assets = addBuiltInGeoItems(assets)
|
?: return
|
||||||
val item = assets.getOrNull(position) ?: return
|
val file = extDir.listFiles()?.find { it.name == asset.second.remarks }
|
||||||
// file with name == item.second.remarks
|
|
||||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
AlertDialog.Builder(ownerActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (file != null) {
|
file?.delete()
|
||||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
MmkvManager.removeAssetUrl(guid)
|
||||||
holder.itemUserAssetBinding.assetProperties.text =
|
initAssets()
|
||||||
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
}
|
||||||
} else {
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
// do nothing
|
||||||
}
|
}
|
||||||
|
.show()
|
||||||
if (item.second.locked == true) {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
|
||||||
} else {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
|
||||||
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
|
|
||||||
intent.putExtra("assetId", item.first)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
|
||||||
AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
file?.delete()
|
|
||||||
MmkvManager.removeAssetUrl(item.first)
|
|
||||||
initAssets()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun onShare(url: String) {
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
}
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
return assets.size
|
override fun onRefreshData() {
|
||||||
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
|
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class UserAssetAdapter(
|
||||||
|
private val viewModel: UserAssetViewModel,
|
||||||
|
private val extDir: File,
|
||||||
|
private val adapterListener: BaseAdapterListener?
|
||||||
|
) : RecyclerView.Adapter<UserAssetAdapter.UserAssetViewHolder>() {
|
||||||
|
|
||||||
|
override fun getItemCount() = viewModel.itemCount
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||||
|
return UserAssetViewHolder(
|
||||||
|
ItemRecyclerUserAssetBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||||
|
val item = viewModel.getAsset(position) ?: return
|
||||||
|
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||||
|
holder.itemUserAssetBinding.assetProperties.text =
|
||||||
|
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
||||||
|
} else {
|
||||||
|
holder.itemUserAssetBinding.assetProperties.text =
|
||||||
|
holder.itemUserAssetBinding.root.context.getString(R.string.msg_file_not_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.second.locked == true) {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = if (item.second.url == "file") {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
||||||
|
adapterListener?.onEdit(item.first, position)
|
||||||
|
}
|
||||||
|
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
||||||
|
adapterListener?.onRemove(item.first, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class LogcatViewModel : ViewModel() {
|
||||||
|
private val logsetsAll: MutableList<String> = mutableListOf()
|
||||||
|
private var filteredLogs: List<String> = emptyList()
|
||||||
|
private var currentFilter: String = ""
|
||||||
|
|
||||||
|
fun getAll(): List<String> = filteredLogs
|
||||||
|
|
||||||
|
fun loadLogcat() {
|
||||||
|
try {
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-d")
|
||||||
|
lst.add("-v")
|
||||||
|
lst.add("time")
|
||||||
|
lst.add("-s")
|
||||||
|
lst.add("GoLog,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
||||||
|
|
||||||
|
logsetsAll.clear()
|
||||||
|
logsetsAll.addAll(allText)
|
||||||
|
applyFilter()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
android.util.Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLogcat() {
|
||||||
|
try {
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-c")
|
||||||
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
logsetsAll.clear()
|
||||||
|
filteredLogs = emptyList()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
android.util.Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filter(content: String?) {
|
||||||
|
currentFilter = content?.trim() ?: ""
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyFilter() {
|
||||||
|
filteredLogs = if (currentFilter.isEmpty()) {
|
||||||
|
logsetsAll.toList()
|
||||||
|
} else {
|
||||||
|
logsetsAll.filter { it.contains(currentFilter) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
|
class UserAssetViewModel : ViewModel() {
|
||||||
|
private val assets = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
|
private val builtInGeoFiles = listOf("geosite.dat", "geoip.dat")
|
||||||
|
|
||||||
|
val itemCount: Int
|
||||||
|
get() = assets.size
|
||||||
|
|
||||||
|
fun getAssets(): List<Pair<String, AssetUrlItem>> = assets.toList()
|
||||||
|
|
||||||
|
fun getAsset(position: Int): Pair<String, AssetUrlItem>? = assets.getOrNull(position)
|
||||||
|
|
||||||
|
fun reload(geoFilesSource: String) {
|
||||||
|
val decoded = MmkvManager.decodeAssetUrls()
|
||||||
|
assets.clear()
|
||||||
|
assets.addAll(buildAssetList(decoded, geoFilesSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAssetList(
|
||||||
|
decodedAssets: List<Pair<String, AssetUrlItem>>?,
|
||||||
|
geoFilesSource: String
|
||||||
|
): List<Pair<String, AssetUrlItem>> {
|
||||||
|
val savedAssets = decodedAssets ?: emptyList()
|
||||||
|
val builtInItems = builtInGeoFiles
|
||||||
|
.filter { geoFile -> savedAssets.none { it.second.remarks == geoFile } }
|
||||||
|
.map {
|
||||||
|
Utils.getUuid() to AssetUrlItem(
|
||||||
|
it,
|
||||||
|
String.format(AppConfig.GITHUB_DOWNLOAD_URL, geoFilesSource).concatUrl(it),
|
||||||
|
locked = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return builtInItems + savedAssets
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
|
||||||
|
val snapshot = getAssets()
|
||||||
|
var successCount = 0
|
||||||
|
val failures = mutableListOf<String>()
|
||||||
|
|
||||||
|
snapshot.forEach { (_, item) ->
|
||||||
|
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0)
|
||||||
|
if (portsToTry.any { tryDownload(item, extDir, it) }) {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failures.add(item.remarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GeoDownloadResult(successCount, failures.size, failures)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryDownload(item: AssetUrlItem, extDir: File, httpPort: Int): Boolean {
|
||||||
|
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||||
|
val target = File(extDir, item.remarks)
|
||||||
|
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
|
||||||
|
try {
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
conn.inputStream.use { inputStream ->
|
||||||
|
FileOutputStream(targetTemp).use { output ->
|
||||||
|
inputStream.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetTemp.renameTo(target)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GeoDownloadResult(
|
||||||
|
val successCount: Int,
|
||||||
|
val failureCount: Int,
|
||||||
|
val failedAssets: List<String>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
Submodule hev-socks5-tunnel updated: 09aed689d7...da33382c72
Reference in New Issue
Block a user