RouteSettingsActivity.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /******************************************************************************
  2. * *
  3. * Copyright (C) 2021 by nekohasekai <[email protected]> *
  4. * Copyright (C) 2021 by Max Lv <[email protected]> *
  5. * Copyright (C) 2021 by Mygod Studio <[email protected]> *
  6. * *
  7. * This program is free software: you can redistribute it and/or modify *
  8. * it under the terms of the GNU General Public License as published by *
  9. * the Free Software Foundation, either version 3 of the License, or *
  10. * (at your option) any later version. *
  11. * *
  12. * This program is distributed in the hope that it will be useful, *
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of *
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
  15. * GNU General Public License for more details. *
  16. * *
  17. * You should have received a copy of the GNU General Public License *
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>. *
  19. * *
  20. ******************************************************************************/
  21. package io.nekohasekai.sagernet.ui
  22. import android.app.Activity
  23. import android.content.DialogInterface
  24. import android.content.Intent
  25. import android.os.Bundle
  26. import android.os.Parcelable
  27. import android.view.Menu
  28. import android.view.MenuItem
  29. import android.view.View
  30. import androidx.activity.result.component1
  31. import androidx.activity.result.component2
  32. import androidx.activity.result.contract.ActivityResultContracts
  33. import androidx.annotation.LayoutRes
  34. import androidx.appcompat.app.AlertDialog
  35. import androidx.appcompat.app.AppCompatActivity
  36. import androidx.core.view.ViewCompat
  37. import androidx.preference.EditTextPreference
  38. import androidx.preference.Preference
  39. import androidx.preference.PreferenceDataStore
  40. import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
  41. import com.takisoft.preferencex.PreferenceFragmentCompat
  42. import io.nekohasekai.sagernet.Key
  43. import io.nekohasekai.sagernet.R
  44. import io.nekohasekai.sagernet.database.DataStore
  45. import io.nekohasekai.sagernet.database.ProfileManager
  46. import io.nekohasekai.sagernet.database.RuleEntity
  47. import io.nekohasekai.sagernet.database.SagerDatabase
  48. import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
  49. import io.nekohasekai.sagernet.ktx.Empty
  50. import io.nekohasekai.sagernet.ktx.onMainDispatcher
  51. import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
  52. import io.nekohasekai.sagernet.utils.DirectBoot
  53. import io.nekohasekai.sagernet.widget.ListListener
  54. import io.nekohasekai.sagernet.widget.OutboundPreference
  55. import kotlinx.parcelize.Parcelize
  56. @Suppress("UNCHECKED_CAST")
  57. class RouteSettingsActivity(
  58. @LayoutRes
  59. resId: Int = R.layout.layout_settings_activity,
  60. ) : AppCompatActivity(resId),
  61. OnPreferenceDataStoreChangeListener {
  62. fun init() {
  63. RuleEntity().init()
  64. }
  65. fun RuleEntity.init() {
  66. DataStore.routeName = name
  67. DataStore.routeDomain = domains
  68. DataStore.routeIP = ip
  69. DataStore.routeSourcePort = sourcePort
  70. DataStore.routeNetwork = network
  71. DataStore.routeSource = source
  72. DataStore.routeProtocol = protocol
  73. DataStore.routeOutboundRule = outbound
  74. DataStore.routeOutbound = when (outbound) {
  75. 0L -> 0
  76. -1L -> 1
  77. -2L -> 2
  78. else -> 3
  79. }
  80. }
  81. fun RuleEntity.serialize() {
  82. name = DataStore.routeName
  83. domains = DataStore.routeDomain
  84. ip = DataStore.routeIP
  85. sourcePort = DataStore.routeSourcePort
  86. network = DataStore.routeNetwork
  87. protocol = DataStore.routeProtocol
  88. outbound = when (DataStore.routeOutbound) {
  89. 0 -> 0L
  90. 1 -> -1L
  91. 2 -> -2L
  92. else -> DataStore.routeOutboundRule
  93. }
  94. }
  95. fun needSave(): Boolean {
  96. if (!DataStore.dirty) return false
  97. if (DataStore.routeDomain.isBlank() &&
  98. DataStore.routeIP.isBlank() &&
  99. DataStore.routeSourcePort.isBlank() &&
  100. DataStore.routeNetwork.isBlank() &&
  101. DataStore.routeProtocol.isBlank()
  102. ) {
  103. return false
  104. }
  105. return true
  106. }
  107. fun PreferenceFragmentCompat.createPreferences(
  108. savedInstanceState: Bundle?,
  109. rootKey: String?,
  110. ) {
  111. addPreferencesFromResource(R.xml.route_preferences)
  112. }
  113. lateinit var outbound: OutboundPreference
  114. val selectProfileForAdd = registerForActivityResult(
  115. ActivityResultContracts.StartActivityForResult()
  116. ) { (resultCode, data) ->
  117. if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {
  118. val profile = ProfileManager.getProfile(
  119. data!!.getLongExtra(
  120. ProfileSelectActivity.EXTRA_PROFILE_ID,
  121. 0
  122. )
  123. ) ?: return@runOnDefaultDispatcher
  124. DataStore.routeOutboundRule = profile.id
  125. onMainDispatcher {
  126. outbound.value = "3"
  127. }
  128. }
  129. }
  130. fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
  131. outbound = findPreference(Key.ROUTE_OUTBOUND)!!
  132. outbound.setOnPreferenceChangeListener { _, newValue ->
  133. if (newValue.toString() == "3") {
  134. selectProfileForAdd.launch(
  135. Intent(
  136. this@RouteSettingsActivity,
  137. ProfileSelectActivity::class.java
  138. )
  139. )
  140. false
  141. } else true
  142. }
  143. }
  144. fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {
  145. return false
  146. }
  147. class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
  148. override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
  149. setTitle(R.string.unsaved_changes_prompt)
  150. setPositiveButton(R.string.yes) { _, _ ->
  151. runOnDefaultDispatcher {
  152. (requireActivity() as RouteSettingsActivity).saveAndExit()
  153. }
  154. }
  155. setNegativeButton(R.string.no) { _, _ ->
  156. requireActivity().finish()
  157. }
  158. setNeutralButton(android.R.string.cancel, null)
  159. }
  160. }
  161. @Parcelize
  162. data class ProfileIdArg(val ruleId: Long) : Parcelable
  163. class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {
  164. override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
  165. setTitle(R.string.delete_route_prompt)
  166. setPositiveButton(R.string.yes) { _, _ ->
  167. runOnDefaultDispatcher {
  168. ProfileManager.deleteRule(arg.ruleId)
  169. }
  170. requireActivity().finish()
  171. }
  172. setNegativeButton(R.string.no, null)
  173. }
  174. }
  175. companion object {
  176. const val EXTRA_ROUTE_ID = "id"
  177. }
  178. override fun onCreate(savedInstanceState: Bundle?) {
  179. super.onCreate(savedInstanceState)
  180. setSupportActionBar(findViewById(R.id.toolbar))
  181. supportActionBar?.apply {
  182. setTitle(R.string.cag_route)
  183. setDisplayHomeAsUpEnabled(true)
  184. setHomeAsUpIndicator(R.drawable.ic_navigation_close)
  185. }
  186. if (savedInstanceState == null) {
  187. val editingId = intent.getLongExtra(EXTRA_ROUTE_ID, 0L)
  188. DataStore.editingId = editingId
  189. runOnDefaultDispatcher {
  190. if (editingId == 0L) {
  191. init()
  192. } else {
  193. val ruleEntity = SagerDatabase.rulesDao.getById(editingId)
  194. if (ruleEntity == null) {
  195. onMainDispatcher {
  196. finish()
  197. }
  198. return@runOnDefaultDispatcher
  199. }
  200. ruleEntity.init()
  201. }
  202. onMainDispatcher {
  203. supportFragmentManager.beginTransaction()
  204. .replace(R.id.settings,
  205. MyPreferenceFragmentCompat().apply {
  206. activity = this@RouteSettingsActivity
  207. })
  208. .commit()
  209. DataStore.dirty = false
  210. DataStore.profileCacheStore.registerChangeListener(this@RouteSettingsActivity)
  211. }
  212. }
  213. }
  214. }
  215. suspend fun saveAndExit() {
  216. if (!needSave()) {
  217. onMainDispatcher {
  218. AlertDialog.Builder(this@RouteSettingsActivity)
  219. .setTitle(R.string.empty_route)
  220. .setMessage(R.string.empty_route_notice)
  221. .setPositiveButton(android.R.string.ok, null)
  222. .show()
  223. }
  224. return
  225. }
  226. val editingId = DataStore.editingId
  227. if (editingId == 0L) {
  228. ProfileManager.createRule(RuleEntity().apply { serialize() })
  229. } else {
  230. val entity = SagerDatabase.rulesDao.getById(DataStore.editingId)
  231. if (entity == null) {
  232. finish()
  233. return
  234. }
  235. ProfileManager.updateRule(entity.apply { serialize() })
  236. }
  237. if (editingId == DataStore.selectedProxy && DataStore.directBootAware) DirectBoot.update()
  238. finish()
  239. }
  240. val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }
  241. override fun onCreateOptionsMenu(menu: Menu?): Boolean {
  242. menuInflater.inflate(R.menu.profile_config_menu, menu)
  243. return true
  244. }
  245. override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
  246. override fun onBackPressed() {
  247. if (needSave()) {
  248. UnsavedChangesDialogFragment().apply { key() }
  249. .show(supportFragmentManager, null)
  250. } else super.onBackPressed()
  251. }
  252. override fun onSupportNavigateUp(): Boolean {
  253. if (!super.onSupportNavigateUp()) finish()
  254. return true
  255. }
  256. override fun onDestroy() {
  257. DataStore.profileCacheStore.unregisterChangeListener(this)
  258. super.onDestroy()
  259. }
  260. override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
  261. if (key != Key.PROFILE_DIRTY) {
  262. DataStore.dirty = true
  263. }
  264. }
  265. class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {
  266. lateinit var activity: RouteSettingsActivity
  267. override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
  268. preferenceManager.preferenceDataStore = DataStore.profileCacheStore
  269. activity.apply {
  270. createPreferences(savedInstanceState, rootKey)
  271. }
  272. }
  273. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  274. super.onViewCreated(view, savedInstanceState)
  275. ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)
  276. activity.apply {
  277. viewCreated(view, savedInstanceState)
  278. }
  279. }
  280. override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
  281. R.id.action_delete -> {
  282. if (DataStore.editingId == 0L) {
  283. requireActivity().finish()
  284. } else {
  285. DeleteConfirmationDialogFragment().apply {
  286. arg(ProfileIdArg(DataStore.editingId))
  287. key()
  288. }.show(parentFragmentManager, null)
  289. }
  290. true
  291. }
  292. R.id.action_apply -> {
  293. runOnDefaultDispatcher {
  294. activity.saveAndExit()
  295. }
  296. true
  297. }
  298. else -> false
  299. }
  300. override fun onDisplayPreferenceDialog(preference: Preference) {
  301. activity.apply {
  302. if (displayPreferenceDialog(preference)) return
  303. }
  304. super.onDisplayPreferenceDialog(preference)
  305. }
  306. }
  307. object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
  308. override fun provideSummary(preference: EditTextPreference): CharSequence {
  309. return if (preference.text.isNullOrBlank()) {
  310. preference.context.getString(androidx.preference.R.string.not_set)
  311. } else {
  312. "\u2022".repeat(preference.text.length)
  313. }
  314. }
  315. }
  316. }