ConfigurationFragment.kt 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686
  1. /******************************************************************************
  2. * *
  3. * Copyright (C) 2021 by nekohasekai <[email protected]> *
  4. * *
  5. * This program is free software: you can redistribute it and/or modify *
  6. * it under the terms of the GNU General Public License as published by *
  7. * the Free Software Foundation, either version 3 of the License, or *
  8. * (at your option) any later version. *
  9. * *
  10. * This program is distributed in the hope that it will be useful, *
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of *
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
  13. * GNU General Public License for more details. *
  14. * *
  15. * You should have received a copy of the GNU General Public License *
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>. *
  17. * *
  18. ******************************************************************************/
  19. package io.nekohasekai.sagernet.ui
  20. import android.content.DialogInterface
  21. import android.content.Intent
  22. import android.graphics.Color
  23. import android.net.Uri
  24. import android.os.Bundle
  25. import android.os.SystemClock
  26. import android.provider.OpenableColumns
  27. import android.text.format.Formatter
  28. import android.text.method.LinkMovementMethod
  29. import android.text.util.Linkify
  30. import android.view.*
  31. import android.widget.ImageView
  32. import android.widget.LinearLayout
  33. import android.widget.TextView
  34. import androidx.activity.result.contract.ActivityResultContracts
  35. import androidx.appcompat.widget.PopupMenu
  36. import androidx.appcompat.widget.Toolbar
  37. import androidx.core.view.isGone
  38. import androidx.core.view.isVisible
  39. import androidx.core.view.size
  40. import androidx.fragment.app.Fragment
  41. import androidx.recyclerview.widget.DefaultItemAnimator
  42. import androidx.recyclerview.widget.ItemTouchHelper
  43. import androidx.recyclerview.widget.LinearLayoutManager
  44. import androidx.recyclerview.widget.RecyclerView
  45. import androidx.viewpager2.adapter.FragmentStateAdapter
  46. import androidx.viewpager2.widget.ViewPager2
  47. import com.google.android.material.dialog.MaterialAlertDialogBuilder
  48. import com.google.android.material.tabs.TabLayout
  49. import com.google.android.material.tabs.TabLayoutMediator
  50. import io.nekohasekai.sagernet.*
  51. import io.nekohasekai.sagernet.aidl.TrafficStats
  52. import io.nekohasekai.sagernet.bg.BaseService
  53. import io.nekohasekai.sagernet.bg.test.LocalDnsInstance
  54. import io.nekohasekai.sagernet.bg.test.UrlTest
  55. import io.nekohasekai.sagernet.database.*
  56. import io.nekohasekai.sagernet.databinding.LayoutProfileBinding
  57. import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding
  58. import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding
  59. import io.nekohasekai.sagernet.fmt.AbstractBean
  60. import io.nekohasekai.sagernet.fmt.toUniversalLink
  61. import io.nekohasekai.sagernet.fmt.v2ray.toV2rayN
  62. import io.nekohasekai.sagernet.group.RawUpdater
  63. import io.nekohasekai.sagernet.ktx.*
  64. import io.nekohasekai.sagernet.plugin.PluginManager
  65. import io.nekohasekai.sagernet.ui.profile.*
  66. import io.nekohasekai.sagernet.widget.QRCodeDialog
  67. import io.nekohasekai.sagernet.widget.UndoSnackbarManager
  68. import kotlinx.coroutines.*
  69. import kotlinx.coroutines.sync.Mutex
  70. import kotlinx.coroutines.sync.withLock
  71. import libcore.Libcore
  72. import java.net.InetAddress
  73. import java.net.InetSocketAddress
  74. import java.net.Socket
  75. import java.net.UnknownHostException
  76. import java.util.concurrent.ConcurrentLinkedQueue
  77. import java.util.zip.ZipInputStream
  78. class ConfigurationFragment @JvmOverloads constructor(
  79. val select: Boolean = false,
  80. val selectedItem: ProxyEntity? = null,
  81. ) : ToolbarFragment(R.layout.layout_group_list),
  82. PopupMenu.OnMenuItemClickListener,
  83. Toolbar.OnMenuItemClickListener {
  84. lateinit var adapter: GroupPagerAdapter
  85. lateinit var tabLayout: TabLayout
  86. lateinit var groupPager: ViewPager2
  87. val selectedGroup get() = if (tabLayout.isGone) adapter.groupList[0] else adapter.groupList[tabLayout.selectedTabPosition]
  88. val alwaysShowAddress by lazy { DataStore.alwaysShowAddress }
  89. val securityAdvisory by lazy { DataStore.securityAdvisory }
  90. val updateSelectedCallback = object : ViewPager2.OnPageChangeCallback() {
  91. override fun onPageScrolled(
  92. position: Int, positionOffset: Float, positionOffsetPixels: Int
  93. ) {
  94. if (adapter.groupList.size > position) {
  95. DataStore.selectedGroup = adapter.groupList[position].id
  96. }
  97. }
  98. }
  99. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  100. super.onViewCreated(view, savedInstanceState)
  101. if (!select) {
  102. toolbar.inflateMenu(R.menu.add_profile_menu)
  103. toolbar.setOnMenuItemClickListener(this)
  104. } else {
  105. toolbar.setTitle(R.string.select_profile)
  106. toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
  107. toolbar.setNavigationOnClickListener {
  108. requireActivity().finish()
  109. }
  110. }
  111. groupPager = view.findViewById(R.id.group_pager)
  112. tabLayout = view.findViewById(R.id.group_tab)
  113. adapter = GroupPagerAdapter()
  114. ProfileManager.addListener(adapter)
  115. GroupManager.addListener(adapter)
  116. groupPager.adapter = adapter
  117. groupPager.offscreenPageLimit = 2
  118. TabLayoutMediator(tabLayout, groupPager) { tab, position ->
  119. if (adapter.groupList.size > position) {
  120. tab.text = adapter.groupList[position].displayName()
  121. }
  122. tab.view.setOnLongClickListener { // clear toast
  123. true
  124. }
  125. }.attach()
  126. toolbar.setOnClickListener {
  127. val fragment = (childFragmentManager.findFragmentByTag("f" + selectedGroup.id) as GroupFragment?)
  128. if (fragment != null) {
  129. val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy
  130. val selectedProfileIndex = fragment.adapter.configurationIdList.indexOf(
  131. selectedProxy
  132. )
  133. if (selectedProfileIndex != -1) {
  134. val layoutManager = fragment.layoutManager
  135. val first = layoutManager.findFirstVisibleItemPosition()
  136. val last = layoutManager.findLastVisibleItemPosition()
  137. if (selectedProfileIndex !in first..last) {
  138. fragment.configurationListView.scrollTo(selectedProfileIndex, true)
  139. return@setOnClickListener
  140. }
  141. }
  142. fragment.configurationListView.scrollTo(0)
  143. }
  144. }
  145. }
  146. override fun onDestroy() {
  147. if (::adapter.isInitialized) {
  148. GroupManager.removeListener(adapter)
  149. ProfileManager.removeListener(adapter)
  150. }
  151. super.onDestroy()
  152. }
  153. override fun onKeyDown(ketCode: Int, event: KeyEvent): Boolean {
  154. val fragment = (childFragmentManager.findFragmentByTag("f" + selectedGroup.id) as GroupFragment?)
  155. fragment?.configurationListView?.apply {
  156. if (!hasFocus()) requestFocus()
  157. }
  158. return super.onKeyDown(ketCode, event)
  159. }
  160. val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) { file ->
  161. if (file != null) runOnDefaultDispatcher {
  162. try {
  163. val fileName = requireContext().contentResolver.query(file, null, null, null, null)
  164. ?.use { cursor ->
  165. cursor.moveToFirst()
  166. cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
  167. .let(cursor::getString)
  168. }
  169. val proxies = mutableListOf<AbstractBean>()
  170. if (fileName != null && fileName.endsWith(".zip")) {
  171. // try parse wireguard zip
  172. val zip = ZipInputStream(requireContext().contentResolver.openInputStream(file)!!)
  173. while (true) {
  174. val entry = zip.nextEntry ?: break
  175. if (entry.isDirectory) continue
  176. val fileText = zip.bufferedReader().readText()
  177. RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }
  178. zip.closeEntry()
  179. }
  180. runCatching {
  181. zip.close()
  182. }
  183. } else {
  184. val fileText = requireContext().contentResolver.openInputStream(file)!!.use {
  185. it.bufferedReader().readText()
  186. }
  187. RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }
  188. }
  189. if (proxies.isEmpty()) onMainDispatcher {
  190. snackbar(getString(R.string.no_proxies_found_in_file)).show()
  191. } else import(proxies)
  192. } catch (e: SubscriptionFoundException) {
  193. (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
  194. } catch (e: Exception) {
  195. Logs.w(e)
  196. onMainDispatcher {
  197. snackbar(e.readableMessage).show()
  198. }
  199. }
  200. }
  201. }
  202. suspend fun import(proxies: List<AbstractBean>) {
  203. val targetId = DataStore.selectedGroupForImport()
  204. val targetIndex = adapter.groupList.indexOfFirst { it.id == targetId }
  205. for (proxy in proxies) {
  206. ProfileManager.createProfile(targetId, proxy)
  207. }
  208. onMainDispatcher {
  209. if (adapter.groupList.isEmpty() || selectedGroup.id != targetId) {
  210. if (targetIndex != -1) {
  211. tabLayout.getTabAt(targetIndex)?.select()
  212. } else {
  213. DataStore.selectedGroup = targetId
  214. adapter.reload()
  215. }
  216. }
  217. snackbar(
  218. requireContext().resources.getQuantityString(
  219. R.plurals.added, proxies.size, proxies.size
  220. )
  221. ).show()
  222. }
  223. }
  224. override fun onMenuItemClick(item: MenuItem): Boolean {
  225. when (item.itemId) {
  226. R.id.action_scan_qr_code -> {
  227. startActivity(Intent(context, ScannerActivity::class.java))
  228. }
  229. R.id.action_import_clipboard -> {
  230. val text = SagerNet.getClipboardText()
  231. if (text.isBlank()) {
  232. snackbar(getString(R.string.clipboard_empty)).show()
  233. } else runOnDefaultDispatcher {
  234. try {
  235. val proxies = RawUpdater.parseRaw(text)
  236. if (proxies.isNullOrEmpty()) onMainDispatcher {
  237. snackbar(getString(R.string.no_proxies_found_in_clipboard)).show()
  238. } else import(proxies)
  239. } catch (e: SubscriptionFoundException) {
  240. (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
  241. } catch (e: Exception) {
  242. Logs.w(e)
  243. onMainDispatcher {
  244. snackbar(e.readableMessage).show()
  245. }
  246. }
  247. }
  248. }
  249. R.id.action_import_file -> {
  250. startFilesForResult(importFile, "*/*")
  251. }
  252. R.id.action_new_socks -> {
  253. startActivity(Intent(requireActivity(), SocksSettingsActivity::class.java))
  254. }
  255. R.id.action_new_http -> {
  256. startActivity(Intent(requireActivity(), HttpSettingsActivity::class.java))
  257. }
  258. R.id.action_new_ss -> {
  259. startActivity(Intent(requireActivity(), ShadowsocksSettingsActivity::class.java))
  260. }
  261. R.id.action_new_ssr -> {
  262. startActivity(Intent(requireActivity(), ShadowsocksRSettingsActivity::class.java))
  263. }
  264. R.id.action_new_vmess -> {
  265. startActivity(Intent(requireActivity(), VMessSettingsActivity::class.java))
  266. }
  267. R.id.action_new_vless -> {
  268. startActivity(Intent(requireActivity(), VLESSSettingsActivity::class.java))
  269. }
  270. R.id.action_new_trojan -> {
  271. startActivity(Intent(requireActivity(), TrojanSettingsActivity::class.java))
  272. }
  273. R.id.action_new_trojan_go -> {
  274. startActivity(Intent(requireActivity(), TrojanGoSettingsActivity::class.java))
  275. }
  276. R.id.action_new_naive -> {
  277. startActivity(Intent(requireActivity(), NaiveSettingsActivity::class.java))
  278. }
  279. R.id.action_new_ping_tunnel -> {
  280. startActivity(Intent(requireActivity(), PingTunnelSettingsActivity::class.java))
  281. }
  282. R.id.action_new_relay_baton -> {
  283. startActivity(Intent(requireActivity(), RelayBatonSettingsActivity::class.java))
  284. }
  285. R.id.action_new_brook -> {
  286. startActivity(Intent(requireActivity(), BrookSettingsActivity::class.java))
  287. }
  288. R.id.action_new_hysteria -> {
  289. startActivity(Intent(requireActivity(), HysteriaSettingsActivity::class.java))
  290. }
  291. R.id.action_new_ssh -> {
  292. startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java))
  293. }
  294. R.id.action_new_wg -> {
  295. startActivity(Intent(requireActivity(), WireGuardSettingsActivity::class.java))
  296. }
  297. R.id.action_new_config -> {
  298. startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java))
  299. }
  300. R.id.action_new_chain -> {
  301. startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java))
  302. }
  303. R.id.action_new_balancer -> {
  304. startActivity(Intent(requireActivity(), BalancerSettingsActivity::class.java))
  305. }
  306. R.id.action_clear_traffic_statistics -> {
  307. runOnDefaultDispatcher {
  308. val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
  309. val toClear = mutableListOf<ProxyEntity>()
  310. if (profiles.isNotEmpty()) for (profile in profiles) {
  311. if (profile.tx != 0L || profile.rx != 0L) {
  312. profile.tx = 0
  313. profile.rx = 0
  314. toClear.add(profile)
  315. }
  316. }
  317. if (toClear.isNotEmpty()) {
  318. ProfileManager.updateProfile(toClear)
  319. }
  320. }
  321. }
  322. R.id.action_connection_test_clear_results -> {
  323. runOnDefaultDispatcher {
  324. val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
  325. val toClear = mutableListOf<ProxyEntity>()
  326. if (profiles.isNotEmpty()) for (profile in profiles) {
  327. if (profile.status != 0) {
  328. profile.status = 0
  329. profile.ping = 0
  330. profile.error = null
  331. toClear.add(profile)
  332. }
  333. }
  334. if (toClear.isNotEmpty()) {
  335. ProfileManager.updateProfile(toClear)
  336. }
  337. }
  338. }
  339. R.id.action_connection_icmp_ping -> {
  340. pingTest(true)
  341. }
  342. R.id.action_connection_tcp_ping -> {
  343. pingTest(false)
  344. }
  345. R.id.action_connection_url_test -> {
  346. urlTest()
  347. }
  348. R.id.action_filter_groups -> {
  349. runOnDefaultDispatcher filter@{
  350. val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!
  351. if (group.subscription?.type != SubscriptionType.OOCv1) {
  352. snackbar(getString(R.string.group_filter_ns)).show()
  353. return@filter
  354. }
  355. val subscription = group.subscription!!
  356. val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
  357. val groups = profiles.mapNotNull { it.requireBean().group }
  358. .toSet()
  359. .toTypedArray()
  360. val checked = groups.map { it in subscription.selectedGroups }.toBooleanArray()
  361. if (groups.isEmpty()) {
  362. snackbar(getString(R.string.group_filter_groups_nf)).show()
  363. return@filter
  364. }
  365. onMainDispatcher {
  366. MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)
  367. .setMultiChoiceItems(groups, checked) { _, which, isChecked ->
  368. val selected = groups[which]
  369. if (isChecked) {
  370. subscription.selectedGroups.add(selected)
  371. } else {
  372. subscription.selectedGroups.remove(selected)
  373. }
  374. }
  375. .setPositiveButton(android.R.string.ok) { _, _ ->
  376. runOnDefaultDispatcher {
  377. GroupManager.updateGroup(group)
  378. }
  379. }
  380. .setNegativeButton(android.R.string.cancel, null)
  381. .show()
  382. }
  383. }
  384. }
  385. R.id.group_filter_owners -> {
  386. runOnDefaultDispatcher filter@{
  387. val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!
  388. if (group.subscription?.type != SubscriptionType.OOCv1) {
  389. snackbar(getString(R.string.group_filter_ns)).show()
  390. return@filter
  391. }
  392. val subscription = group.subscription!!
  393. val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())
  394. val owners = profiles.mapNotNull { it.requireBean().owner }
  395. .toSet()
  396. .toTypedArray()
  397. val checked = owners.map { it in subscription.selectedOwners }.toBooleanArray()
  398. if (owners.isEmpty()) {
  399. snackbar(getString(R.string.group_filter_owners_nf)).show()
  400. return@filter
  401. }
  402. onMainDispatcher {
  403. MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)
  404. .setMultiChoiceItems(owners, checked) { _, which, isChecked ->
  405. val selected = owners[which]
  406. if (isChecked) {
  407. subscription.selectedOwners.add(selected)
  408. } else {
  409. subscription.selectedOwners.remove(selected)
  410. }
  411. }
  412. .setPositiveButton(android.R.string.ok) { _, _ ->
  413. runOnDefaultDispatcher {
  414. GroupManager.updateGroup(group)
  415. }
  416. }
  417. .setNegativeButton(android.R.string.cancel, null)
  418. .show()
  419. }
  420. }
  421. }
  422. R.id.action_filter_tags -> {
  423. runOnDefaultDispatcher filter@{
  424. val group = DataStore.currentGroup()
  425. if (group.subscription?.type != SubscriptionType.OOCv1) {
  426. snackbar(getString(R.string.group_filter_ns)).show()
  427. return@filter
  428. }
  429. val subscription = group.subscription!!
  430. val profiles = SagerDatabase.proxyDao.getByGroup(group.id)
  431. val groups = profiles.flatMap { it.requireBean().tags ?: listOf() }
  432. .toSet()
  433. .toTypedArray()
  434. val checked = groups.map { it in subscription.selectedTags }.toBooleanArray()
  435. if (groups.isEmpty()) {
  436. snackbar(getString(R.string.group_filter_tags_nf)).show()
  437. return@filter
  438. }
  439. onMainDispatcher {
  440. MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_tags)
  441. .setMultiChoiceItems(groups, checked) { _, which, isChecked ->
  442. val selected = groups[which]
  443. if (isChecked) {
  444. subscription.selectedTags.add(selected)
  445. } else {
  446. subscription.selectedTags.remove(selected)
  447. }
  448. }
  449. .setPositiveButton(android.R.string.ok) { _, _ ->
  450. runOnDefaultDispatcher {
  451. GroupManager.updateGroup(group)
  452. }
  453. }
  454. .setNegativeButton(android.R.string.cancel, null)
  455. .show()
  456. }
  457. }
  458. }
  459. }
  460. return true
  461. }
  462. inner class TestDialog {
  463. val binding = LayoutProgressListBinding.inflate(layoutInflater)
  464. val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
  465. .setNegativeButton(android.R.string.cancel) { _, _ ->
  466. cancel()
  467. }
  468. .setCancelable(false)
  469. lateinit var cancel: () -> Unit
  470. val results = ArrayList<ProxyEntity>()
  471. val adapter = TestAdapter()
  472. suspend fun insert(profile: ProxyEntity) {
  473. binding.listView.post {
  474. results.add(profile)
  475. adapter.notifyItemInserted(results.size - 1)
  476. binding.listView.scrollToPosition(results.size - 1)
  477. }
  478. }
  479. suspend fun update(profile: ProxyEntity) {
  480. binding.listView.post {
  481. val index = results.indexOf(profile)
  482. adapter.notifyItemChanged(index)
  483. }
  484. }
  485. init {
  486. binding.listView.layoutManager = FixedLinearLayoutManager(binding.listView)
  487. binding.listView.itemAnimator = DefaultItemAnimator()
  488. binding.listView.adapter = adapter
  489. }
  490. inner class TestAdapter : RecyclerView.Adapter<TestResultHolder>() {
  491. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
  492. TestResultHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))
  493. override fun onBindViewHolder(holder: TestResultHolder, position: Int) {
  494. holder.bind(results[position])
  495. }
  496. override fun getItemCount() = results.size
  497. }
  498. inner class TestResultHolder(val binding: LayoutProfileBinding) : RecyclerView.ViewHolder(
  499. binding.root
  500. ) {
  501. init {
  502. binding.edit.isGone = true
  503. binding.share.isGone = true
  504. }
  505. fun bind(profile: ProxyEntity) {
  506. binding.profileName.text = profile.displayName()
  507. binding.profileType.text = profile.displayType()
  508. when (profile.status) {
  509. -1 -> {
  510. binding.profileStatus.text = profile.error
  511. binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
  512. }
  513. 0 -> {
  514. binding.profileStatus.setText(R.string.connection_test_testing)
  515. binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
  516. }
  517. 1 -> {
  518. binding.profileStatus.text = getString(R.string.available, profile.ping)
  519. binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))
  520. }
  521. 2 -> {
  522. binding.profileStatus.text = profile.error
  523. binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
  524. }
  525. 3 -> {
  526. binding.profileStatus.setText(R.string.unavailable)
  527. binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
  528. }
  529. }
  530. if (profile.status == 3) {
  531. binding.content.setOnClickListener {
  532. alert(profile.error ?: "<?>").show()
  533. }
  534. } else {
  535. binding.content.setOnClickListener {}
  536. }
  537. }
  538. }
  539. }
  540. fun stopService() {
  541. if (SagerNet.started) SagerNet.stopService()
  542. }
  543. @Suppress("EXPERIMENTAL_API_USAGE")
  544. fun pingTest(icmpPing: Boolean) {
  545. stopService()
  546. val test = TestDialog()
  547. val testJobs = mutableListOf<Job>()
  548. val dialog = test.builder.show()
  549. val mainJob = runOnDefaultDispatcher {
  550. val group = DataStore.currentGroup()
  551. var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
  552. if (group.subscription?.type == SubscriptionType.OOCv1) {
  553. val subscription = group.subscription!!
  554. if (subscription.selectedGroups.isNotEmpty()) {
  555. profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }
  556. }
  557. if (subscription.selectedOwners.isNotEmpty()) {
  558. profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }
  559. }
  560. if (subscription.selectedTags.isNotEmpty()) {
  561. profilesUnfiltered = profilesUnfiltered.filter { profile ->
  562. profile.requireBean().tags.containsAll(
  563. subscription.selectedTags
  564. )
  565. }
  566. }
  567. }
  568. val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
  569. val testPool = newFixedThreadPoolContext(5, "Connection test pool")
  570. repeat(5) {
  571. testJobs.add(launch(testPool) {
  572. while (isActive) {
  573. val profile = profiles.poll() ?: break
  574. if (icmpPing) {
  575. if (!profile.requireBean().canICMPing()) {
  576. profile.status = -1
  577. profile.error = app.getString(R.string.connection_test_icmp_ping_unavailable)
  578. test.insert(profile)
  579. continue
  580. }
  581. } else {
  582. if (!profile.requireBean().canTCPing()) {
  583. profile.status = -1
  584. profile.error = app.getString(R.string.connection_test_tcp_ping_unavailable)
  585. test.insert(profile)
  586. continue
  587. }
  588. }
  589. profile.status = 0
  590. test.insert(profile)
  591. var address = profile.requireBean().serverAddress
  592. if (!address.isIpAddress()) {
  593. try {
  594. InetAddress.getAllByName(address).apply {
  595. if (isNotEmpty()) {
  596. address = this[0].hostAddress
  597. }
  598. }
  599. } catch (ignored: UnknownHostException) {
  600. }
  601. }
  602. if (!isActive) break
  603. if (!address.isIpAddress()) {
  604. profile.status = 2
  605. profile.error = app.getString(R.string.connection_test_domain_not_found)
  606. test.update(profile)
  607. continue
  608. }
  609. try {
  610. if (icmpPing) {
  611. val result = Libcore.icmpPing(
  612. address, 5000
  613. )
  614. if (!isActive) break
  615. if (result != -1) {
  616. profile.status = 1
  617. profile.ping = result
  618. } else {
  619. profile.status = 2
  620. profile.error = getString(R.string.connection_test_unreachable)
  621. }
  622. test.update(profile)
  623. } else {
  624. val socket = Socket()
  625. try {
  626. socket.soTimeout = 5000
  627. socket.bind(InetSocketAddress(0))
  628. protectFromVpn(socket.fileDescriptor.int)
  629. val start = SystemClock.elapsedRealtime()
  630. socket.connect(
  631. InetSocketAddress(
  632. address, profile.requireBean().serverPort
  633. ), 5000
  634. )
  635. if (!isActive) break
  636. profile.status = 1
  637. profile.ping = (SystemClock.elapsedRealtime() - start).toInt()
  638. test.update(profile)
  639. } finally {
  640. runCatching {
  641. socket.close()
  642. }
  643. }
  644. }
  645. } catch (e: Exception) {
  646. if (!isActive) break
  647. val message = e.readableMessage
  648. if (icmpPing) {
  649. profile.status = 2
  650. profile.error = getString(R.string.connection_test_unreachable)
  651. } else {
  652. profile.status = 2
  653. when {
  654. !message.contains("failed:") -> profile.error = getString(R.string.connection_test_timeout)
  655. else -> when {
  656. message.contains("ECONNREFUSED") -> {
  657. profile.error = getString(R.string.connection_test_refused)
  658. }
  659. message.contains("ENETUNREACH") -> {
  660. profile.error = getString(R.string.connection_test_unreachable)
  661. }
  662. else -> {
  663. profile.status = 3
  664. profile.error = message
  665. }
  666. }
  667. }
  668. }
  669. test.update(profile)
  670. }
  671. }
  672. })
  673. }
  674. testJobs.joinAll()
  675. testPool.close()
  676. ProfileManager.updateProfile(test.results.filter { it.status != 0 })
  677. onMainDispatcher {
  678. test.binding.progressCircular.isGone = true
  679. dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)
  680. }
  681. }
  682. test.cancel = {
  683. mainJob.cancel()
  684. testJobs.forEach { it.cancel() }
  685. runOnDefaultDispatcher {
  686. ProfileManager.updateProfile(test.results.filter { it.status != 0 })
  687. }
  688. }
  689. }
  690. @Suppress("EXPERIMENTAL_API_USAGE")
  691. fun urlTest() {
  692. stopService()
  693. val test = TestDialog()
  694. val dialog = test.builder.show()
  695. val testJobs = mutableListOf<Job>()
  696. val dnsInstance = LocalDnsInstance()
  697. val mainJob = runOnDefaultDispatcher {
  698. val group = DataStore.currentGroup()
  699. var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
  700. if (group.subscription?.type == SubscriptionType.OOCv1) {
  701. val subscription = group.subscription!!
  702. if (subscription.selectedGroups.isNotEmpty()) {
  703. profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }
  704. }
  705. if (subscription.selectedOwners.isNotEmpty()) {
  706. profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }
  707. }
  708. if (subscription.selectedTags.isNotEmpty()) {
  709. profilesUnfiltered = profilesUnfiltered.filter { profile ->
  710. profile.requireBean().tags.containsAll(
  711. subscription.selectedTags
  712. )
  713. }
  714. }
  715. }
  716. val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
  717. val urlTest = UrlTest()
  718. dnsInstance.launch()
  719. repeat(5) {
  720. testJobs.add(launch {
  721. while (isActive) {
  722. val profile = profiles.poll() ?: break
  723. profile.status = 0
  724. test.insert(profile)
  725. try {
  726. val result = urlTest.doTest(profile)
  727. profile.status = 1
  728. profile.ping = result
  729. } catch (e: PluginManager.PluginNotFoundException) {
  730. profile.status = 2
  731. profile.error = e.readableMessage
  732. } catch (e: Exception) {
  733. profile.status = 3
  734. profile.error = e.readableMessage
  735. }
  736. test.update(profile)
  737. ProfileManager.updateProfile(profile)
  738. }
  739. })
  740. }
  741. testJobs.joinAll()
  742. runCatching {
  743. dnsInstance.close()
  744. }
  745. onMainDispatcher {
  746. test.binding.progressCircular.isGone = true
  747. dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)
  748. }
  749. }
  750. test.cancel = {
  751. runCatching {
  752. dnsInstance.close()
  753. }
  754. mainJob.cancel()
  755. runOnDefaultDispatcher {
  756. GroupManager.postReload(DataStore.currentGroupId())
  757. }
  758. }
  759. }
  760. inner class GroupPagerAdapter : FragmentStateAdapter(this),
  761. ProfileManager.Listener,
  762. GroupManager.Listener {
  763. var selectedGroupIndex = 0
  764. var groupList: ArrayList<ProxyGroup> = ArrayList()
  765. fun reload() {
  766. if (!select) {
  767. groupPager.unregisterOnPageChangeCallback(updateSelectedCallback)
  768. }
  769. runOnDefaultDispatcher {
  770. var newGroupList = ArrayList(SagerDatabase.groupDao.allGroups())
  771. if (newGroupList.isEmpty()) {
  772. SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))
  773. newGroupList = ArrayList(SagerDatabase.groupDao.allGroups())
  774. }
  775. newGroupList.find { it.ungrouped }?.let {
  776. if (SagerDatabase.proxyDao.countByGroup(it.id) == 0L) {
  777. newGroupList.remove(it)
  778. }
  779. }
  780. var selectedGroup = selectedItem?.groupId ?: DataStore.currentGroupId()
  781. var set = false
  782. if (selectedGroup > 0L) {
  783. selectedGroupIndex = newGroupList.indexOfFirst { it.id == selectedGroup }
  784. set = true
  785. } else if (groupList.size == 1) {
  786. selectedGroup = groupList[0].id
  787. if (DataStore.selectedGroup != selectedGroup) {
  788. DataStore.selectedGroup = selectedGroup
  789. }
  790. }
  791. groupPager.post {
  792. groupList = newGroupList
  793. notifyDataSetChanged()
  794. if (set) groupPager.setCurrentItem(selectedGroupIndex, false)
  795. val hideTab = groupList.size < 2
  796. tabLayout.isGone = hideTab
  797. toolbar.elevation = if (hideTab) 0F else dp2px(4).toFloat()
  798. if (!select) {
  799. groupPager.registerOnPageChangeCallback(updateSelectedCallback)
  800. }
  801. }
  802. }
  803. }
  804. init {
  805. reload()
  806. }
  807. override fun getItemCount(): Int {
  808. return groupList.size
  809. }
  810. override fun createFragment(position: Int): Fragment {
  811. return GroupFragment().apply {
  812. proxyGroup = groupList[position]
  813. if (position == selectedGroupIndex) {
  814. selected = true
  815. }
  816. }
  817. }
  818. override fun getItemId(position: Int): Long {
  819. return groupList[position].id
  820. }
  821. override fun containsItem(itemId: Long): Boolean {
  822. return groupList.any { it.id == itemId }
  823. }
  824. override suspend fun groupAdd(group: ProxyGroup) {
  825. tabLayout.post {
  826. groupList.add(group)
  827. if (groupList.any { !it.ungrouped }) tabLayout.post {
  828. tabLayout.visibility = View.VISIBLE
  829. }
  830. notifyItemInserted(groupList.size - 1)
  831. tabLayout.getTabAt(groupList.size - 1)?.select()
  832. }
  833. }
  834. override suspend fun groupRemoved(groupId: Long) {
  835. val index = groupList.indexOfFirst { it.id == groupId }
  836. if (index == -1) return
  837. tabLayout.post {
  838. groupList.removeAt(index)
  839. notifyItemRemoved(index)
  840. }
  841. }
  842. override suspend fun groupUpdated(group: ProxyGroup) {
  843. val index = groupList.indexOfFirst { it.id == group.id }
  844. if (index == -1) return
  845. tabLayout.post {
  846. tabLayout.getTabAt(index)?.text = group.displayName()
  847. }
  848. }
  849. override suspend fun groupUpdated(groupId: Long) = Unit
  850. override suspend fun onAdd(profile: ProxyEntity) {
  851. if (groupList.find { it.id == profile.groupId } == null) {
  852. DataStore.selectedGroup = profile.groupId
  853. reload()
  854. }
  855. }
  856. override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) = Unit
  857. override suspend fun onUpdated(profile: ProxyEntity) = Unit
  858. override suspend fun onRemoved(groupId: Long, profileId: Long) {
  859. val group = groupList.find { it.id == groupId } ?: return
  860. if (group.ungrouped && SagerDatabase.proxyDao.countByGroup(groupId) == 0L) {
  861. reload()
  862. }
  863. }
  864. }
  865. class GroupFragment : Fragment() {
  866. lateinit var proxyGroup: ProxyGroup
  867. var selected = false
  868. var scrolled = false
  869. override fun onCreateView(
  870. inflater: LayoutInflater,
  871. container: ViewGroup?,
  872. savedInstanceState: Bundle?,
  873. ): View? {
  874. return LayoutProfileListBinding.inflate(inflater).root
  875. }
  876. lateinit var undoManager: UndoSnackbarManager<ProxyEntity>
  877. lateinit var adapter: ConfigurationAdapter
  878. override fun onSaveInstanceState(outState: Bundle) {
  879. super.onSaveInstanceState(outState)
  880. if (::proxyGroup.isInitialized) {
  881. outState.putParcelable("proxyGroup", proxyGroup)
  882. }
  883. }
  884. override fun onViewStateRestored(savedInstanceState: Bundle?) {
  885. super.onViewStateRestored(savedInstanceState)
  886. savedInstanceState?.getParcelable<ProxyGroup>("proxyGroup")?.also {
  887. proxyGroup = it
  888. onViewCreated(requireView(), null)
  889. }
  890. }
  891. private val isEnabled: Boolean
  892. get() {
  893. return ((activity as? MainActivity)
  894. ?: return false).state.let { it.canStop || it == BaseService.State.Stopped }
  895. }
  896. private fun isProfileEditable(id: Long): Boolean {
  897. return ((activity as? MainActivity)
  898. ?: return false).state == BaseService.State.Stopped || id != DataStore.selectedProxy
  899. }
  900. lateinit var layoutManager: LinearLayoutManager
  901. lateinit var configurationListView: RecyclerView
  902. val select by lazy { (parentFragment as ConfigurationFragment).select }
  903. val selectedItem by lazy { (parentFragment as ConfigurationFragment).selectedItem }
  904. override fun onResume() {
  905. super.onResume()
  906. if (::configurationListView.isInitialized && configurationListView.size == 0) {
  907. configurationListView.adapter = adapter
  908. runOnDefaultDispatcher {
  909. adapter.reloadProfiles()
  910. }
  911. } else if (!::configurationListView.isInitialized) {
  912. onViewCreated(requireView(), null)
  913. }
  914. checkOrderMenu()
  915. configurationListView.requestFocus()
  916. }
  917. fun checkOrderMenu() {
  918. if (select) return
  919. val pf = requireParentFragment() as? ToolbarFragment ?: return
  920. val menu = pf.toolbar.menu
  921. val origin = menu.findItem(R.id.action_order_origin)
  922. val byName = menu.findItem(R.id.action_order_by_name)
  923. val byDelay = menu.findItem(R.id.action_order_by_delay)
  924. when (proxyGroup.order) {
  925. GroupOrder.ORIGIN -> {
  926. origin.isChecked = true
  927. }
  928. GroupOrder.BY_NAME -> {
  929. byName.isChecked = true
  930. }
  931. GroupOrder.BY_DELAY -> {
  932. byDelay.isChecked = true
  933. }
  934. }
  935. fun updateTo(order: Int) {
  936. if (proxyGroup.order == order) return
  937. runOnDefaultDispatcher {
  938. proxyGroup.order = order
  939. GroupManager.updateGroup(proxyGroup)
  940. }
  941. }
  942. origin.setOnMenuItemClickListener {
  943. it.isChecked = true
  944. updateTo(GroupOrder.ORIGIN)
  945. true
  946. }
  947. byName.setOnMenuItemClickListener {
  948. it.isChecked = true
  949. updateTo(GroupOrder.BY_NAME)
  950. true
  951. }
  952. byDelay.setOnMenuItemClickListener {
  953. it.isChecked = true
  954. updateTo(GroupOrder.BY_DELAY)
  955. true
  956. }
  957. }
  958. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  959. if (!::proxyGroup.isInitialized) return
  960. configurationListView = view.findViewById(R.id.configuration_list)
  961. layoutManager = FixedLinearLayoutManager(configurationListView)
  962. configurationListView.layoutManager = layoutManager
  963. adapter = ConfigurationAdapter()
  964. ProfileManager.addListener(adapter)
  965. GroupManager.addListener(adapter)
  966. configurationListView.adapter = adapter
  967. configurationListView.setItemViewCacheSize(20)
  968. if (!select && proxyGroup.type == GroupType.BASIC) {
  969. undoManager = UndoSnackbarManager(activity as MainActivity, adapter)
  970. ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
  971. ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START
  972. ) {
  973. override fun getSwipeDirs(
  974. recyclerView: RecyclerView,
  975. viewHolder: RecyclerView.ViewHolder,
  976. ): Int {
  977. return if (isProfileEditable((viewHolder as ConfigurationHolder).entity.id)) {
  978. super.getSwipeDirs(recyclerView, viewHolder)
  979. } else 0
  980. }
  981. override fun getDragDirs(
  982. recyclerView: RecyclerView,
  983. viewHolder: RecyclerView.ViewHolder,
  984. ) = if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
  985. override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
  986. val index = viewHolder.bindingAdapterPosition
  987. adapter.remove(index)
  988. undoManager.remove(index to (viewHolder as ConfigurationHolder).entity)
  989. }
  990. override fun onMove(
  991. recyclerView: RecyclerView,
  992. viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,
  993. ): Boolean {
  994. adapter.move(
  995. viewHolder.bindingAdapterPosition, target.bindingAdapterPosition
  996. )
  997. return true
  998. }
  999. override fun clearView(
  1000. recyclerView: RecyclerView,
  1001. viewHolder: RecyclerView.ViewHolder,
  1002. ) {
  1003. super.clearView(recyclerView, viewHolder)
  1004. adapter.commitMove()
  1005. }
  1006. }).attachToRecyclerView(configurationListView)
  1007. }
  1008. }
  1009. override fun onDestroy() {
  1010. if (::adapter.isInitialized) {
  1011. ProfileManager.removeListener(adapter)
  1012. GroupManager.removeListener(adapter)
  1013. }
  1014. super.onDestroy()
  1015. if (!::undoManager.isInitialized) return
  1016. undoManager.flush()
  1017. }
  1018. inner class ConfigurationAdapter : RecyclerView.Adapter<ConfigurationHolder>(),
  1019. ProfileManager.Listener,
  1020. GroupManager.Listener,
  1021. UndoSnackbarManager.Interface<ProxyEntity> {
  1022. init {
  1023. setHasStableIds(true)
  1024. }
  1025. var configurationIdList: MutableList<Long> = mutableListOf()
  1026. val configurationList = HashMap<Long, ProxyEntity>()
  1027. private fun getItem(profileId: Long): ProxyEntity {
  1028. var profile = configurationList[profileId]
  1029. if (profile == null) {
  1030. profile = ProfileManager.getProfile(profileId)
  1031. if (profile != null) {
  1032. configurationList[profileId] = profile
  1033. }
  1034. }
  1035. return profile!!
  1036. }
  1037. private fun getItemAt(index: Int) = getItem(configurationIdList[index])
  1038. override fun onCreateViewHolder(
  1039. parent: ViewGroup,
  1040. viewType: Int,
  1041. ): ConfigurationHolder {
  1042. return ConfigurationHolder(
  1043. LayoutInflater.from(parent.context)
  1044. .inflate(R.layout.layout_profile, parent, false)
  1045. )
  1046. }
  1047. override fun getItemId(position: Int): Long {
  1048. return configurationIdList[position]
  1049. }
  1050. override fun onBindViewHolder(holder: ConfigurationHolder, position: Int) {
  1051. try {
  1052. holder.bind(getItemAt(position))
  1053. } catch (ignored: NullPointerException) { // when group deleted
  1054. }
  1055. }
  1056. override fun getItemCount(): Int {
  1057. return configurationIdList.size
  1058. }
  1059. private val updated = HashSet<ProxyEntity>()
  1060. fun move(from: Int, to: Int) {
  1061. val first = getItemAt(from)
  1062. var previousOrder = first.userOrder
  1063. val (step, range) = if (from < to) Pair(1, from until to) else Pair(
  1064. -1, to + 1 downTo from
  1065. )
  1066. for (i in range) {
  1067. val next = getItemAt(i + step)
  1068. val order = next.userOrder
  1069. next.userOrder = previousOrder
  1070. previousOrder = order
  1071. configurationIdList[i] = next.id
  1072. updated.add(next)
  1073. }
  1074. first.userOrder = previousOrder
  1075. configurationIdList[to] = first.id
  1076. updated.add(first)
  1077. notifyItemMoved(from, to)
  1078. }
  1079. fun commitMove() = runOnDefaultDispatcher {
  1080. updated.forEach { SagerDatabase.proxyDao.updateProxy(it) }
  1081. updated.clear()
  1082. }
  1083. fun remove(pos: Int) {
  1084. configurationIdList.removeAt(pos)
  1085. notifyItemRemoved(pos)
  1086. }
  1087. override fun undo(actions: List<Pair<Int, ProxyEntity>>) {
  1088. for ((index, item) in actions) {
  1089. configurationListView.post {
  1090. configurationList[item.id] = item
  1091. configurationIdList.add(index, item.id)
  1092. notifyItemInserted(index)
  1093. }
  1094. }
  1095. }
  1096. override fun commit(actions: List<Pair<Int, ProxyEntity>>) {
  1097. val profiles = actions.map { it.second }
  1098. runOnDefaultDispatcher {
  1099. for (entity in profiles) {
  1100. ProfileManager.deleteProfile(entity.groupId, entity.id)
  1101. }
  1102. }
  1103. }
  1104. override suspend fun onAdd(profile: ProxyEntity) {
  1105. if (profile.groupId != proxyGroup.id) return
  1106. configurationListView.post {
  1107. if (::undoManager.isInitialized) {
  1108. undoManager.flush()
  1109. }
  1110. val pos = itemCount
  1111. configurationList[profile.id] = profile
  1112. configurationIdList.add(profile.id)
  1113. notifyItemInserted(pos)
  1114. }
  1115. }
  1116. override suspend fun onUpdated(profile: ProxyEntity) {
  1117. if (profile.groupId != proxyGroup.id) return
  1118. val index = configurationIdList.indexOf(profile.id)
  1119. if (index < 0) return
  1120. configurationListView.post {
  1121. if (::undoManager.isInitialized) {
  1122. undoManager.flush()
  1123. }
  1124. configurationList[profile.id] = profile
  1125. notifyItemChanged(index)
  1126. }
  1127. }
  1128. override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) {
  1129. val index = configurationIdList.indexOf(profileId)
  1130. if (index != -1) {
  1131. val holder = layoutManager.findViewByPosition(index)
  1132. ?.let { configurationListView.getChildViewHolder(it) } as ConfigurationHolder?
  1133. if (holder != null) {
  1134. holder.entity.stats = trafficStats
  1135. onMainDispatcher {
  1136. holder.bind(holder.entity)
  1137. }
  1138. }
  1139. }
  1140. }
  1141. override suspend fun onRemoved(groupId: Long, profileId: Long) {
  1142. if (groupId != proxyGroup.id) return
  1143. val index = configurationIdList.indexOf(profileId)
  1144. if (index < 0) return
  1145. configurationListView.post {
  1146. configurationIdList.removeAt(index)
  1147. configurationList.remove(profileId)
  1148. notifyItemRemoved(index)
  1149. }
  1150. }
  1151. override suspend fun groupAdd(group: ProxyGroup) = Unit
  1152. override suspend fun groupRemoved(groupId: Long) = Unit
  1153. override suspend fun groupUpdated(group: ProxyGroup) {
  1154. if (group.id != proxyGroup.id) return
  1155. proxyGroup = group
  1156. reloadProfiles()
  1157. }
  1158. override suspend fun groupUpdated(groupId: Long) {
  1159. if (groupId != proxyGroup.id) return
  1160. proxyGroup = SagerDatabase.groupDao.getById(groupId)!!
  1161. reloadProfiles()
  1162. }
  1163. fun reloadProfiles() {
  1164. var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
  1165. val subscription = proxyGroup.subscription
  1166. if (subscription != null) {
  1167. if (subscription.selectedGroups.isNotEmpty()) {
  1168. newProfiles = newProfiles.filter { it.requireBean().group in subscription.selectedGroups }
  1169. }
  1170. if (subscription.selectedOwners.isNotEmpty()) {
  1171. newProfiles = newProfiles.filter { it.requireBean().owner in subscription.selectedOwners }
  1172. }
  1173. if (subscription.selectedTags.isNotEmpty()) {
  1174. newProfiles = newProfiles.filter { profile ->
  1175. profile.requireBean().tags.containsAll(
  1176. subscription.selectedTags
  1177. )
  1178. }
  1179. }
  1180. }
  1181. when (proxyGroup.order) {
  1182. GroupOrder.BY_NAME -> {
  1183. newProfiles = newProfiles.sortedBy { it.displayName() }
  1184. }
  1185. GroupOrder.BY_DELAY -> {
  1186. newProfiles = newProfiles.sortedBy { if (it.status == 1) it.ping else 114514 }
  1187. }
  1188. }
  1189. configurationList.clear()
  1190. configurationList.putAll(newProfiles.associateBy { it.id })
  1191. val newProfileIds = newProfiles.map { it.id }
  1192. var selectedProfileIndex = -1
  1193. if (selected) {
  1194. val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy
  1195. selectedProfileIndex = newProfileIds.indexOf(selectedProxy)
  1196. }
  1197. configurationListView.post {
  1198. configurationIdList.clear()
  1199. configurationIdList.addAll(newProfileIds)
  1200. notifyDataSetChanged()
  1201. if (selectedProfileIndex != -1) {
  1202. configurationListView.scrollTo(selectedProfileIndex, true)
  1203. } else if (newProfiles.isNotEmpty()) {
  1204. configurationListView.scrollTo(0, true)
  1205. }
  1206. }
  1207. }
  1208. }
  1209. val profileAccess = Mutex()
  1210. val reloadAccess = Mutex()
  1211. inner class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view),
  1212. PopupMenu.OnMenuItemClickListener {
  1213. lateinit var entity: ProxyEntity
  1214. val profileName: TextView = view.findViewById(R.id.profile_name)
  1215. val profileType: TextView = view.findViewById(R.id.profile_type)
  1216. val profileAddress: TextView = view.findViewById(R.id.profile_address)
  1217. val profileStatus: TextView = view.findViewById(R.id.profile_status)
  1218. val trafficText: TextView = view.findViewById(R.id.traffic_text)
  1219. val selectedView: LinearLayout = view.findViewById(R.id.selected_view)
  1220. val editButton: ImageView = view.findViewById(R.id.edit)
  1221. val shareLayout: LinearLayout = view.findViewById(R.id.share)
  1222. val shareLayer: LinearLayout = view.findViewById(R.id.share_layer)
  1223. val shareButton: ImageView = view.findViewById(R.id.shareIcon)
  1224. fun bind(proxyEntity: ProxyEntity) {
  1225. val pf = parentFragment as? ConfigurationFragment ?: return
  1226. entity = proxyEntity
  1227. if (select) {
  1228. view.setOnClickListener {
  1229. (requireActivity() as ProfileSelectActivity).returnProfile(proxyEntity.id)
  1230. }
  1231. } else {
  1232. val pa = activity as MainActivity
  1233. view.setOnClickListener {
  1234. runOnDefaultDispatcher {
  1235. var update: Boolean
  1236. var lastSelected: Long
  1237. profileAccess.withLock {
  1238. update = DataStore.selectedProxy != proxyEntity.id
  1239. lastSelected = DataStore.selectedProxy
  1240. DataStore.selectedProxy = proxyEntity.id
  1241. onMainDispatcher {
  1242. selectedView.visibility = View.VISIBLE
  1243. }
  1244. }
  1245. if (update) {
  1246. ProfileManager.postUpdate(lastSelected)
  1247. if (pa.state.canStop && reloadAccess.tryLock()) {
  1248. SagerNet.stopService()
  1249. delay(1000L)
  1250. SagerNet.startService()
  1251. reloadAccess.unlock()
  1252. }
  1253. } else if (SagerNet.isTv) {
  1254. if (SagerNet.started) {
  1255. SagerNet.stopService()
  1256. } else {
  1257. SagerNet.startService()
  1258. }
  1259. }
  1260. }
  1261. }
  1262. }
  1263. profileName.text = proxyEntity.displayName()
  1264. profileType.text = proxyEntity.displayType()
  1265. var rx = proxyEntity.rx
  1266. var tx = proxyEntity.tx
  1267. val stats = proxyEntity.stats
  1268. if (stats != null) {
  1269. rx += stats.rxTotal
  1270. tx += stats.txTotal
  1271. }
  1272. val showTraffic = rx + tx != 0L
  1273. trafficText.isVisible = showTraffic
  1274. if (showTraffic) {
  1275. trafficText.text = view.context.getString(
  1276. R.string.traffic,
  1277. Formatter.formatFileSize(view.context, tx),
  1278. Formatter.formatFileSize(view.context, rx)
  1279. )
  1280. }
  1281. var address = proxyEntity.displayAddress()
  1282. if (showTraffic && address.length >= 30) {
  1283. address = address.substring(0, 27) + "..."
  1284. }
  1285. if (proxyEntity.requireBean().name.isBlank() || !pf.alwaysShowAddress) {
  1286. address = ""
  1287. }
  1288. profileAddress.text = address
  1289. (trafficText.parent as View).isGone = (!showTraffic || proxyEntity.status <= 0) && address.isBlank()
  1290. if (proxyEntity.status <= 0) {
  1291. if (showTraffic) {
  1292. profileStatus.text = trafficText.text
  1293. profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))
  1294. trafficText.text = ""
  1295. } else {
  1296. profileStatus.text = ""
  1297. }
  1298. } else if (proxyEntity.status == 1) {
  1299. profileStatus.text = getString(R.string.available, proxyEntity.ping)
  1300. profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))
  1301. } else {
  1302. profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))
  1303. if (proxyEntity.status == 2) {
  1304. profileStatus.text = proxyEntity.error
  1305. }
  1306. }
  1307. if (proxyEntity.status == 3) {
  1308. profileStatus.setText(R.string.unavailable)
  1309. profileStatus.setOnClickListener {
  1310. alert(proxyEntity.error ?: "<?>").show()
  1311. }
  1312. } else {
  1313. profileStatus.setOnClickListener(null)
  1314. }
  1315. editButton.setOnClickListener {
  1316. it.context.startActivity(
  1317. proxyEntity.settingIntent(
  1318. it.context, proxyGroup.type == GroupType.SUBSCRIPTION
  1319. )
  1320. )
  1321. }
  1322. editButton.isGone = select
  1323. runOnDefaultDispatcher {
  1324. val selected = (selectedItem?.id ?: DataStore.selectedProxy) == proxyEntity.id
  1325. val started = selected && SagerNet.started && DataStore.startedProfile == proxyEntity.id
  1326. onMainDispatcher {
  1327. editButton.isEnabled = !started
  1328. selectedView.visibility = if (selected) View.VISIBLE else View.INVISIBLE
  1329. }
  1330. fun showShare(anchor: View) {
  1331. val popup = PopupMenu(requireContext(), anchor)
  1332. popup.menuInflater.inflate(R.menu.profile_share_menu, popup.menu)
  1333. if (proxyEntity.vmessBean == null) {
  1334. popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_v2rayn_qr)
  1335. popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(R.id.action_v2rayn_clipboard)
  1336. }
  1337. when {
  1338. !proxyEntity.haveLink() -> {
  1339. popup.menu.removeItem(R.id.action_group_qr)
  1340. popup.menu.removeItem(R.id.action_group_clipboard)
  1341. }
  1342. !proxyEntity.haveStandardLink() -> {
  1343. popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_standard_qr)
  1344. popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(
  1345. R.id.action_standard_clipboard
  1346. )
  1347. }
  1348. }
  1349. if (proxyEntity.ptBean != null || proxyEntity.brookBean != null) {
  1350. popup.menu.removeItem(R.id.action_group_configuration)
  1351. }
  1352. popup.setOnMenuItemClickListener(this@ConfigurationHolder)
  1353. popup.show()
  1354. }
  1355. if (!select) {
  1356. val validateResult = if (pf.securityAdvisory) {
  1357. proxyEntity.requireBean().isInsecure()
  1358. } else ResultLocal
  1359. when (validateResult) {
  1360. is ResultInsecure -> onMainDispatcher {
  1361. shareLayout.isVisible = true
  1362. shareLayer.setBackgroundColor(Color.RED)
  1363. shareButton.setImageResource(R.drawable.ic_baseline_warning_24)
  1364. shareButton.setColorFilter(Color.WHITE)
  1365. shareLayout.setOnClickListener {
  1366. MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.insecure)
  1367. .setMessage(resources.openRawResource(validateResult.textRes)
  1368. .bufferedReader()
  1369. .use { it.readText() })
  1370. .setPositiveButton(android.R.string.ok) { _, _ ->
  1371. showShare(it)
  1372. }
  1373. .show()
  1374. .apply {
  1375. findViewById<TextView>(android.R.id.message)?.apply {
  1376. Linkify.addLinks(this, Linkify.WEB_URLS)
  1377. movementMethod = LinkMovementMethod.getInstance()
  1378. }
  1379. }
  1380. }
  1381. }
  1382. is ResultDeprecated -> onMainDispatcher {
  1383. shareLayout.isVisible = true
  1384. shareLayer.setBackgroundColor(Color.YELLOW)
  1385. shareButton.setImageResource(R.drawable.ic_baseline_warning_24)
  1386. shareButton.setColorFilter(Color.GRAY)
  1387. shareLayout.setOnClickListener {
  1388. MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.deprecated)
  1389. .setMessage(resources.openRawResource(validateResult.textRes)
  1390. .bufferedReader()
  1391. .use { it.readText() })
  1392. .setPositiveButton(android.R.string.ok) { _, _ ->
  1393. showShare(it)
  1394. }
  1395. .show()
  1396. .apply {
  1397. findViewById<TextView>(android.R.id.message)?.apply {
  1398. Linkify.addLinks(this, Linkify.WEB_URLS)
  1399. movementMethod = LinkMovementMethod.getInstance()
  1400. }
  1401. }
  1402. }
  1403. }
  1404. else -> onMainDispatcher {
  1405. shareLayer.setBackgroundColor(Color.TRANSPARENT)
  1406. shareButton.setImageResource(R.drawable.ic_social_share)
  1407. shareButton.setColorFilter(Color.GRAY)
  1408. shareButton.isVisible = true
  1409. shareLayout.setOnClickListener {
  1410. showShare(it)
  1411. }
  1412. }
  1413. }
  1414. }
  1415. }
  1416. }
  1417. fun showCode(link: String) {
  1418. QRCodeDialog(link).showAllowingStateLoss(parentFragmentManager)
  1419. }
  1420. fun export(link: String) {
  1421. val success = SagerNet.trySetPrimaryClip(link)
  1422. (activity as MainActivity).snackbar(if (success) R.string.action_export_msg else R.string.action_export_err)
  1423. .show()
  1424. }
  1425. override fun onMenuItemClick(item: MenuItem): Boolean {
  1426. try {
  1427. when (item.itemId) {
  1428. R.id.action_standard_qr -> showCode(entity.toLink()!!)
  1429. R.id.action_standard_clipboard -> export(entity.toLink()!!)
  1430. R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink())
  1431. R.id.action_universal_clipboard -> export(
  1432. entity.requireBean().toUniversalLink()
  1433. )
  1434. R.id.action_v2rayn_qr -> showCode(entity.vmessBean!!.toV2rayN())
  1435. R.id.action_v2rayn_clipboard -> export(entity.vmessBean!!.toV2rayN())
  1436. R.id.action_config_export_clipboard -> export(entity.exportConfig().first)
  1437. R.id.action_config_export_file -> {
  1438. val cfg = entity.exportConfig()
  1439. DataStore.serverConfig = cfg.first
  1440. startFilesForResult(
  1441. (parentFragment as ConfigurationFragment).exportConfig, cfg.second
  1442. )
  1443. }
  1444. }
  1445. } catch (e: Exception) {
  1446. Logs.w(e)
  1447. (activity as MainActivity).snackbar(e.readableMessage).show()
  1448. return true
  1449. }
  1450. return true
  1451. }
  1452. }
  1453. }
  1454. private val exportConfig = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
  1455. if (data != null) {
  1456. runOnDefaultDispatcher {
  1457. try {
  1458. (requireActivity() as MainActivity).contentResolver.openOutputStream(data)!!
  1459. .bufferedWriter()
  1460. .use {
  1461. it.write(DataStore.serverConfig)
  1462. }
  1463. onMainDispatcher {
  1464. snackbar(getString(R.string.action_export_msg)).show()
  1465. }
  1466. } catch (e: Exception) {
  1467. Logs.w(e)
  1468. onMainDispatcher {
  1469. snackbar(e.readableMessage).show()
  1470. }
  1471. }
  1472. }
  1473. }
  1474. }
  1475. }