| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686 |
- /******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai <[email protected]> *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see <http://www.gnu.org/licenses/>. *
- * *
- ******************************************************************************/
- package io.nekohasekai.sagernet.ui
- import android.content.DialogInterface
- import android.content.Intent
- import android.graphics.Color
- import android.net.Uri
- import android.os.Bundle
- import android.os.SystemClock
- import android.provider.OpenableColumns
- import android.text.format.Formatter
- import android.text.method.LinkMovementMethod
- import android.text.util.Linkify
- import android.view.*
- import android.widget.ImageView
- import android.widget.LinearLayout
- import android.widget.TextView
- import androidx.activity.result.contract.ActivityResultContracts
- import androidx.appcompat.widget.PopupMenu
- import androidx.appcompat.widget.Toolbar
- import androidx.core.view.isGone
- import androidx.core.view.isVisible
- import androidx.core.view.size
- import androidx.fragment.app.Fragment
- import androidx.recyclerview.widget.DefaultItemAnimator
- import androidx.recyclerview.widget.ItemTouchHelper
- import androidx.recyclerview.widget.LinearLayoutManager
- import androidx.recyclerview.widget.RecyclerView
- import androidx.viewpager2.adapter.FragmentStateAdapter
- import androidx.viewpager2.widget.ViewPager2
- import com.google.android.material.dialog.MaterialAlertDialogBuilder
- import com.google.android.material.tabs.TabLayout
- import com.google.android.material.tabs.TabLayoutMediator
- import io.nekohasekai.sagernet.*
- import io.nekohasekai.sagernet.aidl.TrafficStats
- import io.nekohasekai.sagernet.bg.BaseService
- import io.nekohasekai.sagernet.bg.test.LocalDnsInstance
- import io.nekohasekai.sagernet.bg.test.UrlTest
- import io.nekohasekai.sagernet.database.*
- import io.nekohasekai.sagernet.databinding.LayoutProfileBinding
- import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding
- import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding
- import io.nekohasekai.sagernet.fmt.AbstractBean
- import io.nekohasekai.sagernet.fmt.toUniversalLink
- import io.nekohasekai.sagernet.fmt.v2ray.toV2rayN
- import io.nekohasekai.sagernet.group.RawUpdater
- import io.nekohasekai.sagernet.ktx.*
- import io.nekohasekai.sagernet.plugin.PluginManager
- import io.nekohasekai.sagernet.ui.profile.*
- import io.nekohasekai.sagernet.widget.QRCodeDialog
- import io.nekohasekai.sagernet.widget.UndoSnackbarManager
- import kotlinx.coroutines.*
- import kotlinx.coroutines.sync.Mutex
- import kotlinx.coroutines.sync.withLock
- import libcore.Libcore
- import java.net.InetAddress
- import java.net.InetSocketAddress
- import java.net.Socket
- import java.net.UnknownHostException
- import java.util.concurrent.ConcurrentLinkedQueue
- import java.util.zip.ZipInputStream
- class ConfigurationFragment @JvmOverloads constructor(
- val select: Boolean = false,
- val selectedItem: ProxyEntity? = null,
- ) : ToolbarFragment(R.layout.layout_group_list),
- PopupMenu.OnMenuItemClickListener,
- Toolbar.OnMenuItemClickListener {
- lateinit var adapter: GroupPagerAdapter
- lateinit var tabLayout: TabLayout
- lateinit var groupPager: ViewPager2
- val selectedGroup get() = if (tabLayout.isGone) adapter.groupList[0] else adapter.groupList[tabLayout.selectedTabPosition]
- val alwaysShowAddress by lazy { DataStore.alwaysShowAddress }
- val securityAdvisory by lazy { DataStore.securityAdvisory }
- val updateSelectedCallback = object : ViewPager2.OnPageChangeCallback() {
- override fun onPageScrolled(
- position: Int, positionOffset: Float, positionOffsetPixels: Int
- ) {
- if (adapter.groupList.size > position) {
- DataStore.selectedGroup = adapter.groupList[position].id
- }
- }
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- if (!select) {
- toolbar.inflateMenu(R.menu.add_profile_menu)
- toolbar.setOnMenuItemClickListener(this)
- } else {
- toolbar.setTitle(R.string.select_profile)
- toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
- toolbar.setNavigationOnClickListener {
- requireActivity().finish()
- }
- }
- groupPager = view.findViewById(R.id.group_pager)
- tabLayout = view.findViewById(R.id.group_tab)
- adapter = GroupPagerAdapter()
- ProfileManager.addListener(adapter)
- GroupManager.addListener(adapter)
- groupPager.adapter = adapter
- groupPager.offscreenPageLimit = 2
- TabLayoutMediator(tabLayout, groupPager) { tab, position ->
- if (adapter.groupList.size > position) {
- tab.text = adapter.groupList[position].displayName()
- }
- tab.view.setOnLongClickListener { // clear toast
- true
- }
- }.attach()
- toolbar.setOnClickListener {
- val fragment = (childFragmentManager.findFragmentByTag("f" + selectedGroup.id) as GroupFragment?)
- if (fragment != null) {
- val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy
- val selectedProfileIndex = fragment.adapter.configurationIdList.indexOf(
- selectedProxy
- )
- if (selectedProfileIndex != -1) {
- val layoutManager = fragment.layoutManager
- val first = layoutManager.findFirstVisibleItemPosition()
- val last = layoutManager.findLastVisibleItemPosition()
- if (selectedProfileIndex !in first..last) {
- fragment.configurationListView.scrollTo(selectedProfileIndex, true)
- return@setOnClickListener
- }
- }
- fragment.configurationListView.scrollTo(0)
- }
- }
- }
- override fun onDestroy() {
- if (::adapter.isInitialized) {
- GroupManager.removeListener(adapter)
- ProfileManager.removeListener(adapter)
- }
- super.onDestroy()
- }
- override fun onKeyDown(ketCode: Int, event: KeyEvent): Boolean {
- val fragment = (childFragmentManager.findFragmentByTag("f" + selectedGroup.id) as GroupFragment?)
- fragment?.configurationListView?.apply {
- if (!hasFocus()) requestFocus()
- }
- return super.onKeyDown(ketCode, event)
- }
- val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) { file ->
- if (file != null) runOnDefaultDispatcher {
- try {
- val fileName = requireContext().contentResolver.query(file, null, null, null, null)
- ?.use { cursor ->
- cursor.moveToFirst()
- cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
- .let(cursor::getString)
- }
- val proxies = mutableListOf<AbstractBean>()
- if (fileName != null && fileName.endsWith(".zip")) {
- // try parse wireguard zip
- val zip = ZipInputStream(requireContext().contentResolver.openInputStream(file)!!)
- while (true) {
- val entry = zip.nextEntry ?: break
- if (entry.isDirectory) continue
- val fileText = zip.bufferedReader().readText()
- RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }
- zip.closeEntry()
- }
- runCatching {
- zip.close()
- }
- } else {
- val fileText = requireContext().contentResolver.openInputStream(file)!!.use {
- it.bufferedReader().readText()
- }
- RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }
- }
- if (proxies.isEmpty()) onMainDispatcher {
- snackbar(getString(R.string.no_proxies_found_in_file)).show()
- } else import(proxies)
- } catch (e: SubscriptionFoundException) {
- (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
- } catch (e: Exception) {
- Logs.w(e)
- onMainDispatcher {
- snackbar(e.readableMessage).show()
- }
- }
- }
- }
- suspend fun import(proxies: List<AbstractBean>) {
- val targetId = DataStore.selectedGroupForImport()
- val targetIndex = adapter.groupList.indexOfFirst { it.id == targetId }
- for (proxy in proxies) {
- ProfileManager.createProfile(targetId, proxy)
- }
- onMainDispatcher {
- if (adapter.groupList.isEmpty() || selectedGroup.id != targetId) {
- if (targetIndex != -1) {
- tabLayout.getTabAt(targetIndex)?.select()
- } else {
- DataStore.selectedGroup = targetId
- adapter.reload()
- }
- }
- snackbar(
- requireContext().resources.getQuantityString(
- R.plurals.added, proxies.size, proxies.size
- )
- ).show()
- }
- }
- override fun onMenuItemClick(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_scan_qr_code -> {
- startActivity(Intent(context, ScannerActivity::class.java))
- }
- R.id.action_import_clipboard -> {
- val text = SagerNet.getClipboardText()
- if (text.isBlank()) {
- snackbar(getString(R.string.clipboard_empty)).show()
- } else runOnDefaultDispatcher {
- try {
- val proxies = RawUpdater.parseRaw(text)
- if (proxies.isNullOrEmpty()) onMainDispatcher {
- snackbar(getString(R.string.no_proxies_found_in_clipboard)).show()
- } else import(proxies)
- } catch (e: SubscriptionFoundException) {
- (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
- } catch (e: Exception) {
- Logs.w(e)
- onMainDispatcher {
- snackbar(e.readableMessage).show()
- }
- }
- }
- }
- R.id.action_import_file -> {
- startFilesForResult(importFile, "*/*")
- }
- R.id.action_new_socks -> {
- startActivity(Intent(requireActivity(), SocksSettingsActivity::class.java))
- }
- R.id.action_new_http -> {
- startActivity(Intent(requireActivity(), HttpSettingsActivity::class.java))
- }
- R.id.action_new_ss -> {
- startActivity(Intent(requireActivity(), ShadowsocksSettingsActivity::class.java))
- }
- R.id.action_new_ssr -> {
- startActivity(Intent(requireActivity(), ShadowsocksRSettingsActivity::class.java))
- }
- R.id.action_new_vmess -> {
- startActivity(Intent(requireActivity(), VMessSettingsActivity::class.java))
- }
- R.id.action_new_vless -> {
- startActivity(Intent(requireActivity(), VLESSSettingsActivity::class.java))
- }
- R.id.action_new_trojan -> {
- startActivity(Intent(requireActivity(), TrojanSettingsActivity::class.java))
- }
- R.id.action_new_trojan_go -> {
- startActivity(Intent(requireActivity(), TrojanGoSettingsActivity::class.java))
- }
- R.id.action_new_naive -> {
- startActivity(Intent(requireActivity(), NaiveSettingsActivity::class.java))
- }
- R.id.action_new_ping_tunnel -> {
- startActivity(Intent(requireActivity(), PingTunnelSettingsActivity::class.java))
- }
- R.id.action_new_relay_baton -> {
- startActivity(Intent(requireActivity(), RelayBatonSettingsActivity::class.java))
- }
- R.id.action_new_brook -> {
- startActivity(Intent(requireActivity(), BrookSettingsActivity::class.java))
- }
- R.id.action_new_hysteria -> {
- startActivity(Intent(requireActivity(), HysteriaSettingsActivity::class.java))
- }
- R.id.action_new_ssh -> {
- startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java))
- }
- R.id.action_new_wg -> {
- startActivity(Intent(requireActivity(), WireGuardSettingsActivity::class.java))
- }
- R.id.action_new_config -> {
- startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java))
- }
- R.id.action_new_chain -> {
- startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java))
- }
- R.id.action_new_balancer -> {
- startActivity(Intent(requireActivity(), BalancerSettingsActivity::class.java))
- }
- R.id.action_clear_traffic_statistics -> {
- runOnDefaultDispatcher {
- val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
- val toClear = mutableListOf<ProxyEntity>()
- if (profiles.isNotEmpty()) for (profile in profiles) {
- if (profile.tx != 0L || profile.rx != 0L) {
- profile.tx = 0
- profile.rx = 0
- toClear.add(profile)
- }
- }
- if (toClear.isNotEmpty()) {
- ProfileManager.updateProfile(toClear)
- }
- }
- }
- R.id.action_connection_test_clear_results -> {
- runOnDefaultDispatcher {
- val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
- val toClear = mutableListOf<ProxyEntity>()
- if (profiles.isNotEmpty()) for (profile in profiles) {
- if (profile.status != 0) {
- profile.status = 0
- profile.ping = 0
- profile.error = null
- toClear.add(profile)
- }
- }
- if (toClear.isNotEmpty()) {
- ProfileManager.updateProfile(toClear)
- }
- }
- }
- R.id.action_connection_icmp_ping -> {
- pingTest(true)
- }
- R.id.action_connection_tcp_ping -> {
- pingTest(false)
- }
- R.id.action_connection_url_test -> {
- urlTest()
- }
- R.id.action_filter_groups -> {
- runOnDefaultDispatcher filter@{
- val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!
- if (group.subscription?.type != SubscriptionType.OOCv1) {
- snackbar(getString(R.string.group_filter_ns)).show()
- return@filter
- }
- val subscription = group.subscription!!
- val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
- val groups = profiles.mapNotNull { it.requireBean().group }
- .toSet()
- .toTypedArray()
- val checked = groups.map { it in subscription.selectedGroups }.toBooleanArray()
- if (groups.isEmpty()) {
- snackbar(getString(R.string.group_filter_groups_nf)).show()
- return@filter
- }
- onMainDispatcher {
- MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)
- .setMultiChoiceItems(groups, checked) { _, which, isChecked ->
- val selected = groups[which]
- if (isChecked) {
- subscription.selectedGroups.add(selected)
- } else {
- subscription.selectedGroups.remove(selected)
- }
- }
- .setPositiveButton(android.R.string.ok) { _, _ ->
- runOnDefaultDispatcher {
- GroupManager.updateGroup(group)
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
- }
- }
- R.id.group_filter_owners -> {
- runOnDefaultDispatcher filter@{
- val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!
- if (group.subscription?.type != SubscriptionType.OOCv1) {
- snackbar(getString(R.string.group_filter_ns)).show()
- return@filter
- }
- val subscription = group.subscription!!
- val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
- val owners = profiles.mapNotNull { it.requireBean().owner }
- .toSet()
- .toTypedArray()
- val checked = owners.map { it in subscription.selectedOwners }.toBooleanArray()
- if (owners.isEmpty()) {
- snackbar(getString(R.string.group_filter_owners_nf)).show()
- return@filter
- }
- onMainDispatcher {
- MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)
- .setMultiChoiceItems(owners, checked) { _, which, isChecked ->
- val selected = owners[which]
- if (isChecked) {
- subscription.selectedOwners.add(selected)
- } else {
- subscription.selectedOwners.remove(selected)
- }
- }
- .setPositiveButton(android.R.string.ok) { _, _ ->
- runOnDefaultDispatcher {
- GroupManager.updateGroup(group)
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
- }
- }
- R.id.action_filter_tags -> {
- runOnDefaultDispatcher filter@{
- val group = DataStore.currentGroup()
- if (group.subscription?.type != SubscriptionType.OOCv1) {
- snackbar(getString(R.string.group_filter_ns)).show()
- return@filter
- }
- val subscription = group.subscription!!
- val profiles = SagerDatabase.proxyDao.getByGroup(group.id)
- val groups = profiles.flatMap { it.requireBean().tags ?: listOf() }
- .toSet()
- .toTypedArray()
- val checked = groups.map { it in subscription.selectedTags }.toBooleanArray()
- if (groups.isEmpty()) {
- snackbar(getString(R.string.group_filter_tags_nf)).show()
- return@filter
- }
- onMainDispatcher {
- MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_tags)
- .setMultiChoiceItems(groups, checked) { _, which, isChecked ->
- val selected = groups[which]
- if (isChecked) {
- subscription.selectedTags.add(selected)
- } else {
- subscription.selectedTags.remove(selected)
- }
- }
- .setPositiveButton(android.R.string.ok) { _, _ ->
- runOnDefaultDispatcher {
- GroupManager.updateGroup(group)
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
- }
- }
- }
- return true
- }
- inner class TestDialog {
- val binding = LayoutProgressListBinding.inflate(layoutInflater)
- val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- cancel()
- }
- .setCancelable(false)
- lateinit var cancel: () -> Unit
- val results = ArrayList<ProxyEntity>()
- val adapter = TestAdapter()
- suspend fun insert(profile: ProxyEntity) {
- binding.listView.post {
- results.add(profile)
- adapter.notifyItemInserted(results.size - 1)
- binding.listView.scrollToPosition(results.size - 1)
- }
- }
- suspend fun update(profile: ProxyEntity) {
- binding.listView.post {
- val index = results.indexOf(profile)
- adapter.notifyItemChanged(index)
- }
- }
- init {
- binding.listView.layoutManager = FixedLinearLayoutManager(binding.listView)
- binding.listView.itemAnimator = DefaultItemAnimator()
- binding.listView.adapter = adapter
- }
- inner class TestAdapter : RecyclerView.Adapter<TestResultHolder>() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
- TestResultHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))
- override fun onBindViewHolder(holder: TestResultHolder, position: Int) {
- holder.bind(results[position])
- }
- override fun getItemCount() = results.size
- }
- inner class TestResultHolder(val binding: LayoutProfileBinding) : RecyclerView.ViewHolder(
- binding.root
- ) {
- init {
- binding.edit.isGone = true
- binding.share.isGone = true
- }
- fun bind(profile: ProxyEntity) {
- binding.profileName.text = profile.displayName()
- binding.profileType.text = profile.displayType()
- when (profile.status) {
- -1 -> {
- binding.profileStatus.text = profile.error
- binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
- }
- 0 -> {
- binding.profileStatus.setText(R.string.connection_test_testing)
- binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
- }
- 1 -> {
- binding.profileStatus.text = getString(R.string.available, profile.ping)
- binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))
- }
- 2 -> {
- binding.profileStatus.text = profile.error
- binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
- }
- 3 -> {
- binding.profileStatus.setText(R.string.unavailable)
- binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
- }
- }
- if (profile.status == 3) {
- binding.content.setOnClickListener {
- alert(profile.error ?: "<?>").show()
- }
- } else {
- binding.content.setOnClickListener {}
- }
- }
- }
- }
- fun stopService() {
- if (SagerNet.started) SagerNet.stopService()
- }
- @Suppress("EXPERIMENTAL_API_USAGE")
- fun pingTest(icmpPing: Boolean) {
- stopService()
- val test = TestDialog()
- val testJobs = mutableListOf<Job>()
- val dialog = test.builder.show()
- val mainJob = runOnDefaultDispatcher {
- val group = DataStore.currentGroup()
- var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
- if (group.subscription?.type == SubscriptionType.OOCv1) {
- val subscription = group.subscription!!
- if (subscription.selectedGroups.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }
- }
- if (subscription.selectedOwners.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }
- }
- if (subscription.selectedTags.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { profile ->
- profile.requireBean().tags.containsAll(
- subscription.selectedTags
- )
- }
- }
- }
- val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
- val testPool = newFixedThreadPoolContext(5, "Connection test pool")
- repeat(5) {
- testJobs.add(launch(testPool) {
- while (isActive) {
- val profile = profiles.poll() ?: break
- if (icmpPing) {
- if (!profile.requireBean().canICMPing()) {
- profile.status = -1
- profile.error = app.getString(R.string.connection_test_icmp_ping_unavailable)
- test.insert(profile)
- continue
- }
- } else {
- if (!profile.requireBean().canTCPing()) {
- profile.status = -1
- profile.error = app.getString(R.string.connection_test_tcp_ping_unavailable)
- test.insert(profile)
- continue
- }
- }
- profile.status = 0
- test.insert(profile)
- var address = profile.requireBean().serverAddress
- if (!address.isIpAddress()) {
- try {
- InetAddress.getAllByName(address).apply {
- if (isNotEmpty()) {
- address = this[0].hostAddress
- }
- }
- } catch (ignored: UnknownHostException) {
- }
- }
- if (!isActive) break
- if (!address.isIpAddress()) {
- profile.status = 2
- profile.error = app.getString(R.string.connection_test_domain_not_found)
- test.update(profile)
- continue
- }
- try {
- if (icmpPing) {
- val result = Libcore.icmpPing(
- address, 5000
- )
- if (!isActive) break
- if (result != -1) {
- profile.status = 1
- profile.ping = result
- } else {
- profile.status = 2
- profile.error = getString(R.string.connection_test_unreachable)
- }
- test.update(profile)
- } else {
- val socket = Socket()
- try {
- socket.soTimeout = 5000
- socket.bind(InetSocketAddress(0))
- protectFromVpn(socket.fileDescriptor.int)
- val start = SystemClock.elapsedRealtime()
- socket.connect(
- InetSocketAddress(
- address, profile.requireBean().serverPort
- ), 5000
- )
- if (!isActive) break
- profile.status = 1
- profile.ping = (SystemClock.elapsedRealtime() - start).toInt()
- test.update(profile)
- } finally {
- runCatching {
- socket.close()
- }
- }
- }
- } catch (e: Exception) {
- if (!isActive) break
- val message = e.readableMessage
- if (icmpPing) {
- profile.status = 2
- profile.error = getString(R.string.connection_test_unreachable)
- } else {
- profile.status = 2
- when {
- !message.contains("failed:") -> profile.error = getString(R.string.connection_test_timeout)
- else -> when {
- message.contains("ECONNREFUSED") -> {
- profile.error = getString(R.string.connection_test_refused)
- }
- message.contains("ENETUNREACH") -> {
- profile.error = getString(R.string.connection_test_unreachable)
- }
- else -> {
- profile.status = 3
- profile.error = message
- }
- }
- }
- }
- test.update(profile)
- }
- }
- })
- }
- testJobs.joinAll()
- testPool.close()
- ProfileManager.updateProfile(test.results.filter { it.status != 0 })
- onMainDispatcher {
- test.binding.progressCircular.isGone = true
- dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)
- }
- }
- test.cancel = {
- mainJob.cancel()
- testJobs.forEach { it.cancel() }
- runOnDefaultDispatcher {
- ProfileManager.updateProfile(test.results.filter { it.status != 0 })
- }
- }
- }
- @Suppress("EXPERIMENTAL_API_USAGE")
- fun urlTest() {
- stopService()
- val test = TestDialog()
- val dialog = test.builder.show()
- val testJobs = mutableListOf<Job>()
- val dnsInstance = LocalDnsInstance()
- val mainJob = runOnDefaultDispatcher {
- val group = DataStore.currentGroup()
- var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
- if (group.subscription?.type == SubscriptionType.OOCv1) {
- val subscription = group.subscription!!
- if (subscription.selectedGroups.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }
- }
- if (subscription.selectedOwners.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }
- }
- if (subscription.selectedTags.isNotEmpty()) {
- profilesUnfiltered = profilesUnfiltered.filter { profile ->
- profile.requireBean().tags.containsAll(
- subscription.selectedTags
- )
- }
- }
- }
- val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
- val urlTest = UrlTest()
- dnsInstance.launch()
- repeat(5) {
- testJobs.add(launch {
- while (isActive) {
- val profile = profiles.poll() ?: break
- profile.status = 0
- test.insert(profile)
- try {
- val result = urlTest.doTest(profile)
- profile.status = 1
- profile.ping = result
- } catch (e: PluginManager.PluginNotFoundException) {
- profile.status = 2
- profile.error = e.readableMessage
- } catch (e: Exception) {
- profile.status = 3
- profile.error = e.readableMessage
- }
- test.update(profile)
- ProfileManager.updateProfile(profile)
- }
- })
- }
- testJobs.joinAll()
- runCatching {
- dnsInstance.close()
- }
- onMainDispatcher {
- test.binding.progressCircular.isGone = true
- dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)
- }
- }
- test.cancel = {
- runCatching {
- dnsInstance.close()
- }
- mainJob.cancel()
- runOnDefaultDispatcher {
- GroupManager.postReload(DataStore.currentGroupId())
- }
- }
- }
- inner class GroupPagerAdapter : FragmentStateAdapter(this),
- ProfileManager.Listener,
- GroupManager.Listener {
- var selectedGroupIndex = 0
- var groupList: ArrayList<ProxyGroup> = ArrayList()
- fun reload() {
- if (!select) {
- groupPager.unregisterOnPageChangeCallback(updateSelectedCallback)
- }
- runOnDefaultDispatcher {
- var newGroupList = ArrayList(SagerDatabase.groupDao.allGroups())
- if (newGroupList.isEmpty()) {
- SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))
- newGroupList = ArrayList(SagerDatabase.groupDao.allGroups())
- }
- newGroupList.find { it.ungrouped }?.let {
- if (SagerDatabase.proxyDao.countByGroup(it.id) == 0L) {
- newGroupList.remove(it)
- }
- }
- var selectedGroup = selectedItem?.groupId ?: DataStore.currentGroupId()
- var set = false
- if (selectedGroup > 0L) {
- selectedGroupIndex = newGroupList.indexOfFirst { it.id == selectedGroup }
- set = true
- } else if (groupList.size == 1) {
- selectedGroup = groupList[0].id
- if (DataStore.selectedGroup != selectedGroup) {
- DataStore.selectedGroup = selectedGroup
- }
- }
- groupPager.post {
- groupList = newGroupList
- notifyDataSetChanged()
- if (set) groupPager.setCurrentItem(selectedGroupIndex, false)
- val hideTab = groupList.size < 2
- tabLayout.isGone = hideTab
- toolbar.elevation = if (hideTab) 0F else dp2px(4).toFloat()
- if (!select) {
- groupPager.registerOnPageChangeCallback(updateSelectedCallback)
- }
- }
- }
- }
- init {
- reload()
- }
- override fun getItemCount(): Int {
- return groupList.size
- }
- override fun createFragment(position: Int): Fragment {
- return GroupFragment().apply {
- proxyGroup = groupList[position]
- if (position == selectedGroupIndex) {
- selected = true
- }
- }
- }
- override fun getItemId(position: Int): Long {
- return groupList[position].id
- }
- override fun containsItem(itemId: Long): Boolean {
- return groupList.any { it.id == itemId }
- }
- override suspend fun groupAdd(group: ProxyGroup) {
- tabLayout.post {
- groupList.add(group)
- if (groupList.any { !it.ungrouped }) tabLayout.post {
- tabLayout.visibility = View.VISIBLE
- }
- notifyItemInserted(groupList.size - 1)
- tabLayout.getTabAt(groupList.size - 1)?.select()
- }
- }
- override suspend fun groupRemoved(groupId: Long) {
- val index = groupList.indexOfFirst { it.id == groupId }
- if (index == -1) return
- tabLayout.post {
- groupList.removeAt(index)
- notifyItemRemoved(index)
- }
- }
- override suspend fun groupUpdated(group: ProxyGroup) {
- val index = groupList.indexOfFirst { it.id == group.id }
- if (index == -1) return
- tabLayout.post {
- tabLayout.getTabAt(index)?.text = group.displayName()
- }
- }
- override suspend fun groupUpdated(groupId: Long) = Unit
- override suspend fun onAdd(profile: ProxyEntity) {
- if (groupList.find { it.id == profile.groupId } == null) {
- DataStore.selectedGroup = profile.groupId
- reload()
- }
- }
- override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) = Unit
- override suspend fun onUpdated(profile: ProxyEntity) = Unit
- override suspend fun onRemoved(groupId: Long, profileId: Long) {
- val group = groupList.find { it.id == groupId } ?: return
- if (group.ungrouped && SagerDatabase.proxyDao.countByGroup(groupId) == 0L) {
- reload()
- }
- }
- }
- class GroupFragment : Fragment() {
- lateinit var proxyGroup: ProxyGroup
- var selected = false
- var scrolled = false
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? {
- return LayoutProfileListBinding.inflate(inflater).root
- }
- lateinit var undoManager: UndoSnackbarManager<ProxyEntity>
- lateinit var adapter: ConfigurationAdapter
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- if (::proxyGroup.isInitialized) {
- outState.putParcelable("proxyGroup", proxyGroup)
- }
- }
- override fun onViewStateRestored(savedInstanceState: Bundle?) {
- super.onViewStateRestored(savedInstanceState)
- savedInstanceState?.getParcelable<ProxyGroup>("proxyGroup")?.also {
- proxyGroup = it
- onViewCreated(requireView(), null)
- }
- }
- private val isEnabled: Boolean
- get() {
- return ((activity as? MainActivity)
- ?: return false).state.let { it.canStop || it == BaseService.State.Stopped }
- }
- private fun isProfileEditable(id: Long): Boolean {
- return ((activity as? MainActivity)
- ?: return false).state == BaseService.State.Stopped || id != DataStore.selectedProxy
- }
- lateinit var layoutManager: LinearLayoutManager
- lateinit var configurationListView: RecyclerView
- val select by lazy { (parentFragment as ConfigurationFragment).select }
- val selectedItem by lazy { (parentFragment as ConfigurationFragment).selectedItem }
- override fun onResume() {
- super.onResume()
- if (::configurationListView.isInitialized && configurationListView.size == 0) {
- configurationListView.adapter = adapter
- runOnDefaultDispatcher {
- adapter.reloadProfiles()
- }
- } else if (!::configurationListView.isInitialized) {
- onViewCreated(requireView(), null)
- }
- checkOrderMenu()
- configurationListView.requestFocus()
- }
- fun checkOrderMenu() {
- if (select) return
- val pf = requireParentFragment() as? ToolbarFragment ?: return
- val menu = pf.toolbar.menu
- val origin = menu.findItem(R.id.action_order_origin)
- val byName = menu.findItem(R.id.action_order_by_name)
- val byDelay = menu.findItem(R.id.action_order_by_delay)
- when (proxyGroup.order) {
- GroupOrder.ORIGIN -> {
- origin.isChecked = true
- }
- GroupOrder.BY_NAME -> {
- byName.isChecked = true
- }
- GroupOrder.BY_DELAY -> {
- byDelay.isChecked = true
- }
- }
- fun updateTo(order: Int) {
- if (proxyGroup.order == order) return
- runOnDefaultDispatcher {
- proxyGroup.order = order
- GroupManager.updateGroup(proxyGroup)
- }
- }
- origin.setOnMenuItemClickListener {
- it.isChecked = true
- updateTo(GroupOrder.ORIGIN)
- true
- }
- byName.setOnMenuItemClickListener {
- it.isChecked = true
- updateTo(GroupOrder.BY_NAME)
- true
- }
- byDelay.setOnMenuItemClickListener {
- it.isChecked = true
- updateTo(GroupOrder.BY_DELAY)
- true
- }
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- if (!::proxyGroup.isInitialized) return
- configurationListView = view.findViewById(R.id.configuration_list)
- layoutManager = FixedLinearLayoutManager(configurationListView)
- configurationListView.layoutManager = layoutManager
- adapter = ConfigurationAdapter()
- ProfileManager.addListener(adapter)
- GroupManager.addListener(adapter)
- configurationListView.adapter = adapter
- configurationListView.setItemViewCacheSize(20)
- if (!select && proxyGroup.type == GroupType.BASIC) {
- undoManager = UndoSnackbarManager(activity as MainActivity, adapter)
- ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
- ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START
- ) {
- override fun getSwipeDirs(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- ): Int {
- return if (isProfileEditable((viewHolder as ConfigurationHolder).entity.id)) {
- super.getSwipeDirs(recyclerView, viewHolder)
- } else 0
- }
- override fun getDragDirs(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- ) = if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- val index = viewHolder.bindingAdapterPosition
- adapter.remove(index)
- undoManager.remove(index to (viewHolder as ConfigurationHolder).entity)
- }
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,
- ): Boolean {
- adapter.move(
- viewHolder.bindingAdapterPosition, target.bindingAdapterPosition
- )
- return true
- }
- override fun clearView(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- ) {
- super.clearView(recyclerView, viewHolder)
- adapter.commitMove()
- }
- }).attachToRecyclerView(configurationListView)
- }
- }
- override fun onDestroy() {
- if (::adapter.isInitialized) {
- ProfileManager.removeListener(adapter)
- GroupManager.removeListener(adapter)
- }
- super.onDestroy()
- if (!::undoManager.isInitialized) return
- undoManager.flush()
- }
- inner class ConfigurationAdapter : RecyclerView.Adapter<ConfigurationHolder>(),
- ProfileManager.Listener,
- GroupManager.Listener,
- UndoSnackbarManager.Interface<ProxyEntity> {
- init {
- setHasStableIds(true)
- }
- var configurationIdList: MutableList<Long> = mutableListOf()
- val configurationList = HashMap<Long, ProxyEntity>()
- private fun getItem(profileId: Long): ProxyEntity {
- var profile = configurationList[profileId]
- if (profile == null) {
- profile = ProfileManager.getProfile(profileId)
- if (profile != null) {
- configurationList[profileId] = profile
- }
- }
- return profile!!
- }
- private fun getItemAt(index: Int) = getItem(configurationIdList[index])
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): ConfigurationHolder {
- return ConfigurationHolder(
- LayoutInflater.from(parent.context)
- .inflate(R.layout.layout_profile, parent, false)
- )
- }
- override fun getItemId(position: Int): Long {
- return configurationIdList[position]
- }
- override fun onBindViewHolder(holder: ConfigurationHolder, position: Int) {
- try {
- holder.bind(getItemAt(position))
- } catch (ignored: NullPointerException) { // when group deleted
- }
- }
- override fun getItemCount(): Int {
- return configurationIdList.size
- }
- private val updated = HashSet<ProxyEntity>()
- fun move(from: Int, to: Int) {
- val first = getItemAt(from)
- var previousOrder = first.userOrder
- val (step, range) = if (from < to) Pair(1, from until to) else Pair(
- -1, to + 1 downTo from
- )
- for (i in range) {
- val next = getItemAt(i + step)
- val order = next.userOrder
- next.userOrder = previousOrder
- previousOrder = order
- configurationIdList[i] = next.id
- updated.add(next)
- }
- first.userOrder = previousOrder
- configurationIdList[to] = first.id
- updated.add(first)
- notifyItemMoved(from, to)
- }
- fun commitMove() = runOnDefaultDispatcher {
- updated.forEach { SagerDatabase.proxyDao.updateProxy(it) }
- updated.clear()
- }
- fun remove(pos: Int) {
- configurationIdList.removeAt(pos)
- notifyItemRemoved(pos)
- }
- override fun undo(actions: List<Pair<Int, ProxyEntity>>) {
- for ((index, item) in actions) {
- configurationListView.post {
- configurationList[item.id] = item
- configurationIdList.add(index, item.id)
- notifyItemInserted(index)
- }
- }
- }
- override fun commit(actions: List<Pair<Int, ProxyEntity>>) {
- val profiles = actions.map { it.second }
- runOnDefaultDispatcher {
- for (entity in profiles) {
- ProfileManager.deleteProfile(entity.groupId, entity.id)
- }
- }
- }
- override suspend fun onAdd(profile: ProxyEntity) {
- if (profile.groupId != proxyGroup.id) return
- configurationListView.post {
- if (::undoManager.isInitialized) {
- undoManager.flush()
- }
- val pos = itemCount
- configurationList[profile.id] = profile
- configurationIdList.add(profile.id)
- notifyItemInserted(pos)
- }
- }
- override suspend fun onUpdated(profile: ProxyEntity) {
- if (profile.groupId != proxyGroup.id) return
- val index = configurationIdList.indexOf(profile.id)
- if (index < 0) return
- configurationListView.post {
- if (::undoManager.isInitialized) {
- undoManager.flush()
- }
- configurationList[profile.id] = profile
- notifyItemChanged(index)
- }
- }
- override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) {
- val index = configurationIdList.indexOf(profileId)
- if (index != -1) {
- val holder = layoutManager.findViewByPosition(index)
- ?.let { configurationListView.getChildViewHolder(it) } as ConfigurationHolder?
- if (holder != null) {
- holder.entity.stats = trafficStats
- onMainDispatcher {
- holder.bind(holder.entity)
- }
- }
- }
- }
- override suspend fun onRemoved(groupId: Long, profileId: Long) {
- if (groupId != proxyGroup.id) return
- val index = configurationIdList.indexOf(profileId)
- if (index < 0) return
- configurationListView.post {
- configurationIdList.removeAt(index)
- configurationList.remove(profileId)
- notifyItemRemoved(index)
- }
- }
- override suspend fun groupAdd(group: ProxyGroup) = Unit
- override suspend fun groupRemoved(groupId: Long) = Unit
- override suspend fun groupUpdated(group: ProxyGroup) {
- if (group.id != proxyGroup.id) return
- proxyGroup = group
- reloadProfiles()
- }
- override suspend fun groupUpdated(groupId: Long) {
- if (groupId != proxyGroup.id) return
- proxyGroup = SagerDatabase.groupDao.getById(groupId)!!
- reloadProfiles()
- }
- fun reloadProfiles() {
- var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
- val subscription = proxyGroup.subscription
- if (subscription != null) {
- if (subscription.selectedGroups.isNotEmpty()) {
- newProfiles = newProfiles.filter { it.requireBean().group in subscription.selectedGroups }
- }
- if (subscription.selectedOwners.isNotEmpty()) {
- newProfiles = newProfiles.filter { it.requireBean().owner in subscription.selectedOwners }
- }
- if (subscription.selectedTags.isNotEmpty()) {
- newProfiles = newProfiles.filter { profile ->
- profile.requireBean().tags.containsAll(
- subscription.selectedTags
- )
- }
- }
- }
- when (proxyGroup.order) {
- GroupOrder.BY_NAME -> {
- newProfiles = newProfiles.sortedBy { it.displayName() }
- }
- GroupOrder.BY_DELAY -> {
- newProfiles = newProfiles.sortedBy { if (it.status == 1) it.ping else 114514 }
- }
- }
- configurationList.clear()
- configurationList.putAll(newProfiles.associateBy { it.id })
- val newProfileIds = newProfiles.map { it.id }
- var selectedProfileIndex = -1
- if (selected) {
- val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy
- selectedProfileIndex = newProfileIds.indexOf(selectedProxy)
- }
- configurationListView.post {
- configurationIdList.clear()
- configurationIdList.addAll(newProfileIds)
- notifyDataSetChanged()
- if (selectedProfileIndex != -1) {
- configurationListView.scrollTo(selectedProfileIndex, true)
- } else if (newProfiles.isNotEmpty()) {
- configurationListView.scrollTo(0, true)
- }
- }
- }
- }
- val profileAccess = Mutex()
- val reloadAccess = Mutex()
- inner class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view),
- PopupMenu.OnMenuItemClickListener {
- lateinit var entity: ProxyEntity
- val profileName: TextView = view.findViewById(R.id.profile_name)
- val profileType: TextView = view.findViewById(R.id.profile_type)
- val profileAddress: TextView = view.findViewById(R.id.profile_address)
- val profileStatus: TextView = view.findViewById(R.id.profile_status)
- val trafficText: TextView = view.findViewById(R.id.traffic_text)
- val selectedView: LinearLayout = view.findViewById(R.id.selected_view)
- val editButton: ImageView = view.findViewById(R.id.edit)
- val shareLayout: LinearLayout = view.findViewById(R.id.share)
- val shareLayer: LinearLayout = view.findViewById(R.id.share_layer)
- val shareButton: ImageView = view.findViewById(R.id.shareIcon)
- fun bind(proxyEntity: ProxyEntity) {
- val pf = parentFragment as? ConfigurationFragment ?: return
- entity = proxyEntity
- if (select) {
- view.setOnClickListener {
- (requireActivity() as ProfileSelectActivity).returnProfile(proxyEntity.id)
- }
- } else {
- val pa = activity as MainActivity
- view.setOnClickListener {
- runOnDefaultDispatcher {
- var update: Boolean
- var lastSelected: Long
- profileAccess.withLock {
- update = DataStore.selectedProxy != proxyEntity.id
- lastSelected = DataStore.selectedProxy
- DataStore.selectedProxy = proxyEntity.id
- onMainDispatcher {
- selectedView.visibility = View.VISIBLE
- }
- }
- if (update) {
- ProfileManager.postUpdate(lastSelected)
- if (pa.state.canStop && reloadAccess.tryLock()) {
- SagerNet.stopService()
- delay(1000L)
- SagerNet.startService()
- reloadAccess.unlock()
- }
- } else if (SagerNet.isTv) {
- if (SagerNet.started) {
- SagerNet.stopService()
- } else {
- SagerNet.startService()
- }
- }
- }
- }
- }
- profileName.text = proxyEntity.displayName()
- profileType.text = proxyEntity.displayType()
- var rx = proxyEntity.rx
- var tx = proxyEntity.tx
- val stats = proxyEntity.stats
- if (stats != null) {
- rx += stats.rxTotal
- tx += stats.txTotal
- }
- val showTraffic = rx + tx != 0L
- trafficText.isVisible = showTraffic
- if (showTraffic) {
- trafficText.text = view.context.getString(
- R.string.traffic,
- Formatter.formatFileSize(view.context, tx),
- Formatter.formatFileSize(view.context, rx)
- )
- }
- var address = proxyEntity.displayAddress()
- if (showTraffic && address.length >= 30) {
- address = address.substring(0, 27) + "..."
- }
- if (proxyEntity.requireBean().name.isBlank() || !pf.alwaysShowAddress) {
- address = ""
- }
- profileAddress.text = address
- (trafficText.parent as View).isGone = (!showTraffic || proxyEntity.status <= 0) && address.isBlank()
- if (proxyEntity.status <= 0) {
- if (showTraffic) {
- profileStatus.text = trafficText.text
- profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
- trafficText.text = ""
- } else {
- profileStatus.text = ""
- }
- } else if (proxyEntity.status == 1) {
- profileStatus.text = getString(R.string.available, proxyEntity.ping)
- profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))
- } else {
- profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
- if (proxyEntity.status == 2) {
- profileStatus.text = proxyEntity.error
- }
- }
- if (proxyEntity.status == 3) {
- profileStatus.setText(R.string.unavailable)
- profileStatus.setOnClickListener {
- alert(proxyEntity.error ?: "<?>").show()
- }
- } else {
- profileStatus.setOnClickListener(null)
- }
- editButton.setOnClickListener {
- it.context.startActivity(
- proxyEntity.settingIntent(
- it.context, proxyGroup.type == GroupType.SUBSCRIPTION
- )
- )
- }
- editButton.isGone = select
- runOnDefaultDispatcher {
- val selected = (selectedItem?.id ?: DataStore.selectedProxy) == proxyEntity.id
- val started = selected && SagerNet.started && DataStore.startedProfile == proxyEntity.id
- onMainDispatcher {
- editButton.isEnabled = !started
- selectedView.visibility = if (selected) View.VISIBLE else View.INVISIBLE
- }
- fun showShare(anchor: View) {
- val popup = PopupMenu(requireContext(), anchor)
- popup.menuInflater.inflate(R.menu.profile_share_menu, popup.menu)
- if (proxyEntity.vmessBean == null) {
- popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_v2rayn_qr)
- popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(R.id.action_v2rayn_clipboard)
- }
- when {
- !proxyEntity.haveLink() -> {
- popup.menu.removeItem(R.id.action_group_qr)
- popup.menu.removeItem(R.id.action_group_clipboard)
- }
- !proxyEntity.haveStandardLink() -> {
- popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_standard_qr)
- popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(
- R.id.action_standard_clipboard
- )
- }
- }
- if (proxyEntity.ptBean != null || proxyEntity.brookBean != null) {
- popup.menu.removeItem(R.id.action_group_configuration)
- }
- popup.setOnMenuItemClickListener(this@ConfigurationHolder)
- popup.show()
- }
- if (!select) {
- val validateResult = if (pf.securityAdvisory) {
- proxyEntity.requireBean().isInsecure()
- } else ResultLocal
- when (validateResult) {
- is ResultInsecure -> onMainDispatcher {
- shareLayout.isVisible = true
- shareLayer.setBackgroundColor(Color.RED)
- shareButton.setImageResource(R.drawable.ic_baseline_warning_24)
- shareButton.setColorFilter(Color.WHITE)
- shareLayout.setOnClickListener {
- MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.insecure)
- .setMessage(resources.openRawResource(validateResult.textRes)
- .bufferedReader()
- .use { it.readText() })
- .setPositiveButton(android.R.string.ok) { _, _ ->
- showShare(it)
- }
- .show()
- .apply {
- findViewById<TextView>(android.R.id.message)?.apply {
- Linkify.addLinks(this, Linkify.WEB_URLS)
- movementMethod = LinkMovementMethod.getInstance()
- }
- }
- }
- }
- is ResultDeprecated -> onMainDispatcher {
- shareLayout.isVisible = true
- shareLayer.setBackgroundColor(Color.YELLOW)
- shareButton.setImageResource(R.drawable.ic_baseline_warning_24)
- shareButton.setColorFilter(Color.GRAY)
- shareLayout.setOnClickListener {
- MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.deprecated)
- .setMessage(resources.openRawResource(validateResult.textRes)
- .bufferedReader()
- .use { it.readText() })
- .setPositiveButton(android.R.string.ok) { _, _ ->
- showShare(it)
- }
- .show()
- .apply {
- findViewById<TextView>(android.R.id.message)?.apply {
- Linkify.addLinks(this, Linkify.WEB_URLS)
- movementMethod = LinkMovementMethod.getInstance()
- }
- }
- }
- }
- else -> onMainDispatcher {
- shareLayer.setBackgroundColor(Color.TRANSPARENT)
- shareButton.setImageResource(R.drawable.ic_social_share)
- shareButton.setColorFilter(Color.GRAY)
- shareButton.isVisible = true
- shareLayout.setOnClickListener {
- showShare(it)
- }
- }
- }
- }
- }
- }
- fun showCode(link: String) {
- QRCodeDialog(link).showAllowingStateLoss(parentFragmentManager)
- }
- fun export(link: String) {
- val success = SagerNet.trySetPrimaryClip(link)
- (activity as MainActivity).snackbar(if (success) R.string.action_export_msg else R.string.action_export_err)
- .show()
- }
- override fun onMenuItemClick(item: MenuItem): Boolean {
- try {
- when (item.itemId) {
- R.id.action_standard_qr -> showCode(entity.toLink()!!)
- R.id.action_standard_clipboard -> export(entity.toLink()!!)
- R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink())
- R.id.action_universal_clipboard -> export(
- entity.requireBean().toUniversalLink()
- )
- R.id.action_v2rayn_qr -> showCode(entity.vmessBean!!.toV2rayN())
- R.id.action_v2rayn_clipboard -> export(entity.vmessBean!!.toV2rayN())
- R.id.action_config_export_clipboard -> export(entity.exportConfig().first)
- R.id.action_config_export_file -> {
- val cfg = entity.exportConfig()
- DataStore.serverConfig = cfg.first
- startFilesForResult(
- (parentFragment as ConfigurationFragment).exportConfig, cfg.second
- )
- }
- }
- } catch (e: Exception) {
- Logs.w(e)
- (activity as MainActivity).snackbar(e.readableMessage).show()
- return true
- }
- return true
- }
- }
- }
- private val exportConfig = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
- if (data != null) {
- runOnDefaultDispatcher {
- try {
- (requireActivity() as MainActivity).contentResolver.openOutputStream(data)!!
- .bufferedWriter()
- .use {
- it.write(DataStore.serverConfig)
- }
- onMainDispatcher {
- snackbar(getString(R.string.action_export_msg)).show()
- }
- } catch (e: Exception) {
- Logs.w(e)
- onMainDispatcher {
- snackbar(e.readableMessage).show()
- }
- }
- }
- }
- }
- }
|