Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa4432584e | |||
| bb1c3d915f | |||
| d8983a4761 | |||
| 9815041c77 | |||
| 41b0983582 | |||
| bb1f677d45 | |||
| de1c8bcfcc | |||
| ca6e963575 | |||
| bfb6813dc7 | |||
| cc2b4eec65 | |||
| dd11569969 | |||
| 11c3dd97c3 | |||
| 92f15150cd | |||
| f8e809da8b | |||
| 12b9349e62 | |||
| a580494bde |
@@ -25,8 +25,8 @@ jobs:
|
||||
uses: android-actions/setup-android@v3.2.0
|
||||
with:
|
||||
log-accepted-android-sdk-licenses: false
|
||||
cmdline-tools-version: '12266719'
|
||||
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
||||
cmdline-tools-version: '13114758'
|
||||
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
|
||||
+1
-1
Submodule AndroidLibXrayLite updated: 16222c3387...df33063839
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 702
|
||||
versionName = "2.0.2"
|
||||
versionCode = 705
|
||||
versionName = "2.0.5"
|
||||
multiDexEnabled = true
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
"name": "xray0",
|
||||
"MTU": 1500,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -34,7 +34,7 @@ data class V2rayConfig(
|
||||
var protocol: String,
|
||||
var listen: String? = null,
|
||||
var settings: InSettingsBean? = null,
|
||||
val sniffing: SniffingBean? = null,
|
||||
var sniffing: SniffingBean? = null,
|
||||
val streamSettings: Any? = null,
|
||||
val allocate: Any? = null
|
||||
) {
|
||||
@@ -88,6 +88,7 @@ data class V2rayConfig(
|
||||
var reserved: List<Int>? = null,
|
||||
var mtu: Int? = null,
|
||||
var obfsPassword: String? = null,
|
||||
var version: Int? = null,
|
||||
) {
|
||||
|
||||
data class VnextBean(
|
||||
|
||||
@@ -26,7 +26,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -96,6 +96,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
outboundBean.settings?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.version = 2
|
||||
}
|
||||
|
||||
val sni = outboundBean.streamSettings?.let {
|
||||
|
||||
@@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||
if (uri.port <= 0) 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.serverPort = uri.port.toString()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ object SocksFmt : FmtBase() {
|
||||
if (uri.idnHost.isEmpty()) 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.serverPort = uri.port.toString()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -163,7 +163,7 @@ object VmessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -25,7 +25,7 @@ object WireguardFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.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.GEOSITE_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.Language
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
@@ -386,6 +387,15 @@ object SettingsManager {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -108,7 +108,7 @@ object V2RayServiceManager {
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||
|
||||
@@ -91,7 +91,7 @@ object V2rayConfigManager {
|
||||
private fun getV2rayCustomConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
|
||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
||||
val result = ConfigResult(true, guid, raw)
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
if (!needTun()) {
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -334,18 +334,18 @@ object V2rayConfigManager {
|
||||
*/
|
||||
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
||||
var assets = ""
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||
if (TextUtils.isEmpty(assets)) {
|
||||
return null
|
||||
}
|
||||
initConfigCache = assets
|
||||
} else {
|
||||
if (needTun()) {
|
||||
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
||||
if (TextUtils.isEmpty(assets)) {
|
||||
return null
|
||||
}
|
||||
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)
|
||||
return config
|
||||
@@ -357,6 +357,10 @@ object V2rayConfigManager {
|
||||
|
||||
//region some sub function
|
||||
|
||||
private fun needTun(): Boolean {
|
||||
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the inbound settings for V2ray.
|
||||
*
|
||||
@@ -395,9 +399,10 @@ object V2rayConfigManager {
|
||||
v2rayConfig.inbounds.add(inbound2)
|
||||
}
|
||||
|
||||
if (!SettingsManager.isUsingHevTun()) {
|
||||
if (needTun()) {
|
||||
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
||||
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
||||
inboundTun?.sniffing = inbound1.sniffing
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
||||
@@ -519,23 +524,25 @@ object V2rayConfigManager {
|
||||
)
|
||||
}
|
||||
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
//hev-socks5-tunnel dns routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("socks"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
if(SettingsManager.isVpnMode()) {
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
//hev-socks5-tunnel dns routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("socks"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("tun"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("tun"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DNS outbound
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
/**
|
||||
* A common Adapter -> host callback interface that includes common actions: edit, remove and refresh.
|
||||
* Extend this interface or define more specific interfaces for different adapters as needed.
|
||||
*/
|
||||
interface BaseAdapterListener {
|
||||
/**
|
||||
* Request the host to edit the specified 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, position: Int)
|
||||
|
||||
/**
|
||||
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
||||
* @param guid Unique identifier (GUID) of the item
|
||||
* @param position Current position in the adapter (optional; host should validate it)
|
||||
*/
|
||||
fun onRemove(guid: String, position: Int)
|
||||
|
||||
/**
|
||||
* Request the host to share the specified URL.
|
||||
* @param url The URL to be shared
|
||||
*/
|
||||
fun onShare(url: String)
|
||||
|
||||
/**
|
||||
* Request the host to refresh data (for example, reload from the ViewModel or call notifyDataSetChanged).
|
||||
*/
|
||||
fun onRefreshData()
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import com.v2ray.ang.dto.GroupMapItem
|
||||
class GroupPagerAdapter(activity: FragmentActivity, var groups: List<GroupMapItem>) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = groups.size
|
||||
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")
|
||||
fun update(groups: List<GroupMapItem>) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -9,7 +8,6 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.TAG
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -51,14 +49,14 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
||||
if (mainViewModel.subscriptionId != subId) {
|
||||
return@observe
|
||||
}
|
||||
Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||
adapter.setData(mainViewModel.serversCache, index)
|
||||
}
|
||||
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
||||
adapter.isRunning = isRunning
|
||||
}
|
||||
|
||||
Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -2,37 +2,34 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||
|
||||
private var logsetsAll: MutableList<String> = mutableListOf()
|
||||
var logsets: MutableList<String> = mutableListOf()
|
||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
||||
private val viewModel: LogcatViewModel by viewModels()
|
||||
private lateinit var adapter: LogcatRecyclerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_logcat))
|
||||
|
||||
adapter = LogcatRecyclerAdapter(viewModel, ::onLogLongClick)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||
@@ -40,58 +37,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
binding.refreshLayout.setOnRefreshListener(this)
|
||||
|
||||
logsets.add(getString(R.string.pull_down_to_refresh))
|
||||
toast(getString(R.string.pull_down_to_refresh))
|
||||
}
|
||||
|
||||
private fun getLogcat() {
|
||||
|
||||
try {
|
||||
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)
|
||||
}
|
||||
private fun onLogLongClick(log: String): Boolean {
|
||||
Utils.setClipboard(this, log)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -104,12 +55,14 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
filterLogs(newText)
|
||||
viewModel.filter(newText)
|
||||
refreshData()
|
||||
return false
|
||||
}
|
||||
})
|
||||
searchView.setOnCloseListener {
|
||||
filterLogs("")
|
||||
viewModel.filter("")
|
||||
refreshData()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -119,33 +72,33 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
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)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.clear_all -> {
|
||||
clearLogcat()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.clearLogcat()
|
||||
withContext(Dispatchers.Main) {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
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() {
|
||||
getLogcat()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.loadLogcat()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
|
||||
@@ -6,17 +6,20 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
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>() {
|
||||
private var mActivity: LogcatActivity = activity
|
||||
class LogcatRecyclerAdapter(
|
||||
private val viewModel: LogcatViewModel,
|
||||
private val onLongClick: ((String) -> Boolean)? = null
|
||||
) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||
|
||||
|
||||
override fun getItemCount() = mActivity.logsets.size
|
||||
override fun getItemCount() = viewModel.getAll().size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
try {
|
||||
val log = mActivity.logsets[position]
|
||||
val logs = viewModel.getAll()
|
||||
val log = logs[position]
|
||||
|
||||
if (log.isEmpty()) {
|
||||
holder.itemSubSettingBinding.logTag.text = ""
|
||||
holder.itemSubSettingBinding.logContent.text = ""
|
||||
@@ -27,8 +30,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
||||
}
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
Utils.setClipboard(mActivity, log)
|
||||
true
|
||||
onLongClick?.invoke(log) ?: false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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.tabs.TabLayoutMediator
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||
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.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
@@ -188,7 +188,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
V2RayServiceManager.stopVService(this)
|
||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
||||
} else if (SettingsManager.isVpnMode()) {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) {
|
||||
startV2Ray()
|
||||
|
||||
@@ -31,7 +31,8 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class RoutingSettingActivity : BaseActivity() {
|
||||
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
||||
|
||||
private val ownerActivity: RoutingSettingActivity
|
||||
get() = this
|
||||
private val viewModel: RoutingSettingsViewModel by viewModels()
|
||||
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
@@ -57,7 +58,7 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
//setContentView(binding.root)
|
||||
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.layoutManager = LinearLayoutManager(this)
|
||||
@@ -202,4 +203,23 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
viewModel.reload()
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -13,12 +12,11 @@ import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
||||
|
||||
class RoutingSettingRecyclerAdapter(
|
||||
val activity: RoutingSettingActivity,
|
||||
private val viewModel: RoutingSettingsViewModel
|
||||
private val viewModel: RoutingSettingsViewModel,
|
||||
private val adapterListener: BaseAdapterListener?
|
||||
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
||||
ItemTouchHelperAdapter {
|
||||
|
||||
private var mActivity: RoutingSettingActivity = activity
|
||||
override fun getItemCount() = viewModel.getAll().size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
@@ -33,10 +31,7 @@ class RoutingSettingRecyclerAdapter(
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(
|
||||
Intent(mActivity, RoutingEditActivity::class.java)
|
||||
.putExtra("position", position)
|
||||
)
|
||||
adapterListener?.onEdit("", position)
|
||||
}
|
||||
|
||||
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||
@@ -76,7 +71,7 @@ class RoutingSettingRecyclerAdapter(
|
||||
}
|
||||
|
||||
override fun onItemMoveCompleted() {
|
||||
mActivity.refreshData()
|
||||
adapterListener?.onRefreshData()
|
||||
}
|
||||
|
||||
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 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?) {
|
||||
// Use MMKV as the storage backend for all Preferences
|
||||
// This prevents inconsistencies between SharedPreferences and MMKV
|
||||
@@ -93,12 +97,21 @@ class SettingsActivity : BaseActivity() {
|
||||
}
|
||||
true
|
||||
}
|
||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateMode(newValue.toString())
|
||||
mode?.setOnPreferenceChangeListener { pref, newValue ->
|
||||
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
|
||||
}
|
||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
|
||||
useHevTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateHevTunSettings(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPreferenceSummaries() {
|
||||
@@ -141,6 +154,8 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updateHevTunSettings(MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true))
|
||||
|
||||
// Initialize mode-dependent UI states
|
||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||
|
||||
@@ -152,11 +167,10 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
// Initialize auto-update interval state
|
||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||
|
||||
}
|
||||
|
||||
private fun updateMode(mode: String?) {
|
||||
val vpn = mode == VPN
|
||||
private fun updateMode(value: String?) {
|
||||
val vpn = value == VPN
|
||||
localDns?.isEnabled = vpn
|
||||
fakeDns?.isEnabled = vpn
|
||||
appendHttpProxy?.isEnabled = vpn
|
||||
@@ -165,6 +179,8 @@ class SettingsActivity : BaseActivity() {
|
||||
vpnBypassLan?.isEnabled = vpn
|
||||
vpnInterfaceAddress?.isEnabled = vpn
|
||||
vpnMtu?.isEnabled = vpn
|
||||
useHevTun?.isEnabled = vpn
|
||||
updateHevTunSettings(false)
|
||||
if (vpn) {
|
||||
updateLocalDns(
|
||||
MmkvManager.decodeSettingsBool(
|
||||
@@ -172,6 +188,12 @@ class SettingsActivity : BaseActivity() {
|
||||
false
|
||||
)
|
||||
)
|
||||
updateHevTunSettings(
|
||||
MmkvManager.decodeSettingsBool(
|
||||
AppConfig.PREF_USE_HEV_TUNNEL,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +257,11 @@ class SettingsActivity : BaseActivity() {
|
||||
fragmentLength?.isEnabled = enabled
|
||||
fragmentInterval?.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun updateHevTunSettings(enabled: Boolean) {
|
||||
hevTunLogLevel?.isEnabled = enabled
|
||||
hevTunRwTimeout?.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
fun onModeHelpClicked(view: View) {
|
||||
|
||||
@@ -3,18 +3,27 @@ package com.v2ray.ang.ui
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -22,17 +31,21 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class SubSettingActivity : BaseActivity() {
|
||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||
|
||||
private val ownerActivity: SubSettingActivity
|
||||
get() = this
|
||||
private val viewModel: SubscriptionsViewModel by viewModels()
|
||||
private lateinit var adapter: SubSettingRecyclerAdapter
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
private val share_method: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.share_sub_method)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
||||
|
||||
adapter = SubSettingRecyclerAdapter(this, viewModel)
|
||||
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
@@ -88,4 +101,62 @@ class SubSettingActivity : BaseActivity() {
|
||||
viewModel.reload()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||
override fun onEdit(guid: String, position: Int) {
|
||||
startActivity(
|
||||
Intent(ownerActivity, SubEditActivity::class.java)
|
||||
.putExtra("subId", guid)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRemove(guid: String, position: Int) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||
AlertDialog.Builder(ownerActivity)
|
||||
.setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
viewModel.remove(guid)
|
||||
refreshData()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
viewModel.remove(guid)
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShare(url: String) {
|
||||
AlertDialog.Builder(ownerActivity)
|
||||
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||
try {
|
||||
when (i) {
|
||||
0 -> {
|
||||
val ivBinding =
|
||||
ItemQrcodeBinding.inflate(LayoutInflater.from(ownerActivity))
|
||||
ivBinding.ivQcode.setImageBitmap(
|
||||
QRCodeDecoder.createQRCode(
|
||||
url
|
||||
|
||||
)
|
||||
)
|
||||
AlertDialog.Builder(ownerActivity).setView(ivBinding.root).show()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
Utils.setClipboard(ownerActivity, url)
|
||||
}
|
||||
|
||||
else -> ownerActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun onRefreshData() {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||
|
||||
class SubSettingRecyclerAdapter(
|
||||
val activity: SubSettingActivity,
|
||||
private val viewModel: SubscriptionsViewModel
|
||||
private val viewModel: SubscriptionsViewModel,
|
||||
private val adapterListener: BaseAdapterListener?
|
||||
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||
|
||||
private var mActivity: SubSettingActivity = activity
|
||||
|
||||
private val share_method: Array<out String> by lazy {
|
||||
mActivity.resources.getStringArray(R.array.share_sub_method)
|
||||
}
|
||||
|
||||
override fun getItemCount() = viewModel.getAll().size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
@@ -45,14 +30,11 @@ class SubSettingRecyclerAdapter(
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(
|
||||
Intent(mActivity, SubEditActivity::class.java)
|
||||
.putExtra("subId", subId)
|
||||
)
|
||||
adapterListener?.onEdit(subId, position)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||
removeSubscription(subId, position)
|
||||
adapterListener?.onRemove(subId, position)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||
@@ -72,58 +54,11 @@ class SubSettingRecyclerAdapter(
|
||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||
AlertDialog.Builder(mActivity)
|
||||
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||
try {
|
||||
when (i) {
|
||||
0 -> {
|
||||
val ivBinding =
|
||||
ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||
ivBinding.ivQcode.setImageBitmap(
|
||||
QRCodeDecoder.createQRCode(
|
||||
subItem.url
|
||||
|
||||
)
|
||||
)
|
||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
Utils.setClipboard(mActivity, subItem.url)
|
||||
}
|
||||
|
||||
else -> mActivity.toast("else")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
||||
}
|
||||
}.show()
|
||||
adapterListener?.onShare(subItem.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubscription(subId: String, position: Int) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
removeSubscriptionSub(subId, position)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
//do noting
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
removeSubscriptionSub(subId, position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSubscriptionSub(subId: String, position: Int) {
|
||||
viewModel.remove(subId)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, viewModel.getAll().size)
|
||||
mActivity.refreshData()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||
return MainViewHolder(
|
||||
ItemRecyclerSubSettingBinding.inflate(
|
||||
@@ -154,7 +89,7 @@ class SubSettingRecyclerAdapter(
|
||||
}
|
||||
|
||||
override fun onItemMoveCompleted() {
|
||||
mActivity.refreshData()
|
||||
adapterListener?.onRefreshData()
|
||||
}
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
|
||||
@@ -9,44 +9,36 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
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.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||
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.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class UserAssetActivity : BaseActivity() {
|
||||
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 builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||
|
||||
private val requestStoragePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
@@ -83,13 +75,13 @@ class UserAssetActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting))
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
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.layoutGeoFilesSources.setOnClickListener {
|
||||
@@ -219,81 +211,25 @@ class UserAssetActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
refreshData()
|
||||
showLoading()
|
||||
toast(R.string.msg_downloading_content)
|
||||
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
|
||||
var resultCount = 0
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
assets.forEach {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val result = viewModel.downloadGeoFiles(extDir, httpPort)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (resultCount > 0) {
|
||||
toast(getString(R.string.title_update_config_count, resultCount))
|
||||
refreshData()
|
||||
if (result.successCount > 0) {
|
||||
toast(getString(R.string.title_update_config_count, result.successCount))
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure))
|
||||
}
|
||||
refreshData()
|
||||
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() {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||
@@ -305,72 +241,41 @@ class UserAssetActivity : BaseActivity() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun refreshData() {
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
viewModel.reload(getGeoFilesSources())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||
return UserAssetViewHolder(
|
||||
ItemRecyclerUserAssetBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||
override fun onEdit(guid: String, position: Int) {
|
||||
startActivity(
|
||||
Intent(ownerActivity, UserAssetUrlActivity::class.java)
|
||||
.putExtra("assetId", guid)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
val item = assets.getOrNull(position) ?: return
|
||||
// file with name == item.second.remarks
|
||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||
override fun onRemove(guid: String, position: Int) {
|
||||
val asset = viewModel.getAsset(position)?.takeIf { it.first == guid }
|
||||
?: viewModel.getAssets().find { it.first == guid }
|
||||
?: return
|
||||
val file = extDir.listFiles()?.find { it.name == asset.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 = getString(R.string.msg_file_not_found)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
AlertDialog.Builder(ownerActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
file?.delete()
|
||||
MmkvManager.removeAssetUrl(guid)
|
||||
initAssets()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
return assets.size
|
||||
override fun onShare(url: String) {
|
||||
}
|
||||
|
||||
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