| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- /******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai <[email protected]> *
- * Copyright (C) 2021 by Max Lv <[email protected]> *
- * Copyright (C) 2021 by Mygod Studio <[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.app.Activity
- import android.content.DialogInterface
- import android.content.Intent
- import android.os.Bundle
- import android.os.Parcelable
- import android.view.Menu
- import android.view.MenuItem
- import android.view.View
- import androidx.activity.result.component1
- import androidx.activity.result.component2
- import androidx.activity.result.contract.ActivityResultContracts
- import androidx.annotation.LayoutRes
- import androidx.appcompat.app.AlertDialog
- import androidx.appcompat.app.AppCompatActivity
- import androidx.core.view.ViewCompat
- import androidx.preference.EditTextPreference
- import androidx.preference.Preference
- import androidx.preference.PreferenceDataStore
- import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
- import com.takisoft.preferencex.PreferenceFragmentCompat
- import io.nekohasekai.sagernet.Key
- import io.nekohasekai.sagernet.R
- import io.nekohasekai.sagernet.database.DataStore
- import io.nekohasekai.sagernet.database.ProfileManager
- import io.nekohasekai.sagernet.database.RuleEntity
- import io.nekohasekai.sagernet.database.SagerDatabase
- import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
- import io.nekohasekai.sagernet.ktx.Empty
- import io.nekohasekai.sagernet.ktx.onMainDispatcher
- import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
- import io.nekohasekai.sagernet.utils.DirectBoot
- import io.nekohasekai.sagernet.widget.ListListener
- import io.nekohasekai.sagernet.widget.OutboundPreference
- import kotlinx.parcelize.Parcelize
- @Suppress("UNCHECKED_CAST")
- class RouteSettingsActivity(
- @LayoutRes
- resId: Int = R.layout.layout_settings_activity,
- ) : AppCompatActivity(resId),
- OnPreferenceDataStoreChangeListener {
- fun init() {
- RuleEntity().init()
- }
- fun RuleEntity.init() {
- DataStore.routeName = name
- DataStore.routeDomain = domains
- DataStore.routeIP = ip
- DataStore.routeSourcePort = sourcePort
- DataStore.routeNetwork = network
- DataStore.routeSource = source
- DataStore.routeProtocol = protocol
- DataStore.routeOutboundRule = outbound
- DataStore.routeOutbound = when (outbound) {
- 0L -> 0
- -1L -> 1
- -2L -> 2
- else -> 3
- }
- }
- fun RuleEntity.serialize() {
- name = DataStore.routeName
- domains = DataStore.routeDomain
- ip = DataStore.routeIP
- sourcePort = DataStore.routeSourcePort
- network = DataStore.routeNetwork
- protocol = DataStore.routeProtocol
- outbound = when (DataStore.routeOutbound) {
- 0 -> 0L
- 1 -> -1L
- 2 -> -2L
- else -> DataStore.routeOutboundRule
- }
- }
- fun needSave(): Boolean {
- if (!DataStore.dirty) return false
- if (DataStore.routeDomain.isBlank() &&
- DataStore.routeIP.isBlank() &&
- DataStore.routeSourcePort.isBlank() &&
- DataStore.routeNetwork.isBlank() &&
- DataStore.routeProtocol.isBlank()
- ) {
- return false
- }
- return true
- }
- fun PreferenceFragmentCompat.createPreferences(
- savedInstanceState: Bundle?,
- rootKey: String?,
- ) {
- addPreferencesFromResource(R.xml.route_preferences)
- }
- lateinit var outbound: OutboundPreference
- val selectProfileForAdd = registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { (resultCode, data) ->
- if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {
- val profile = ProfileManager.getProfile(
- data!!.getLongExtra(
- ProfileSelectActivity.EXTRA_PROFILE_ID,
- 0
- )
- ) ?: return@runOnDefaultDispatcher
- DataStore.routeOutboundRule = profile.id
- onMainDispatcher {
- outbound.value = "3"
- }
- }
- }
- fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
- outbound = findPreference(Key.ROUTE_OUTBOUND)!!
- outbound.setOnPreferenceChangeListener { _, newValue ->
- if (newValue.toString() == "3") {
- selectProfileForAdd.launch(
- Intent(
- this@RouteSettingsActivity,
- ProfileSelectActivity::class.java
- )
- )
- false
- } else true
- }
- }
- fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {
- return false
- }
- class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
- override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
- setTitle(R.string.unsaved_changes_prompt)
- setPositiveButton(R.string.yes) { _, _ ->
- runOnDefaultDispatcher {
- (requireActivity() as RouteSettingsActivity).saveAndExit()
- }
- }
- setNegativeButton(R.string.no) { _, _ ->
- requireActivity().finish()
- }
- setNeutralButton(android.R.string.cancel, null)
- }
- }
- @Parcelize
- data class ProfileIdArg(val ruleId: Long) : Parcelable
- class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {
- override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
- setTitle(R.string.delete_route_prompt)
- setPositiveButton(R.string.yes) { _, _ ->
- runOnDefaultDispatcher {
- ProfileManager.deleteRule(arg.ruleId)
- }
- requireActivity().finish()
- }
- setNegativeButton(R.string.no, null)
- }
- }
- companion object {
- const val EXTRA_ROUTE_ID = "id"
- }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setSupportActionBar(findViewById(R.id.toolbar))
- supportActionBar?.apply {
- setTitle(R.string.cag_route)
- setDisplayHomeAsUpEnabled(true)
- setHomeAsUpIndicator(R.drawable.ic_navigation_close)
- }
- if (savedInstanceState == null) {
- val editingId = intent.getLongExtra(EXTRA_ROUTE_ID, 0L)
- DataStore.editingId = editingId
- runOnDefaultDispatcher {
- if (editingId == 0L) {
- init()
- } else {
- val ruleEntity = SagerDatabase.rulesDao.getById(editingId)
- if (ruleEntity == null) {
- onMainDispatcher {
- finish()
- }
- return@runOnDefaultDispatcher
- }
- ruleEntity.init()
- }
- onMainDispatcher {
- supportFragmentManager.beginTransaction()
- .replace(R.id.settings,
- MyPreferenceFragmentCompat().apply {
- activity = this@RouteSettingsActivity
- })
- .commit()
- DataStore.dirty = false
- DataStore.profileCacheStore.registerChangeListener(this@RouteSettingsActivity)
- }
- }
- }
- }
- suspend fun saveAndExit() {
- if (!needSave()) {
- onMainDispatcher {
- AlertDialog.Builder(this@RouteSettingsActivity)
- .setTitle(R.string.empty_route)
- .setMessage(R.string.empty_route_notice)
- .setPositiveButton(android.R.string.ok, null)
- .show()
- }
- return
- }
- val editingId = DataStore.editingId
- if (editingId == 0L) {
- ProfileManager.createRule(RuleEntity().apply { serialize() })
- } else {
- val entity = SagerDatabase.rulesDao.getById(DataStore.editingId)
- if (entity == null) {
- finish()
- return
- }
- ProfileManager.updateRule(entity.apply { serialize() })
- }
- if (editingId == DataStore.selectedProxy && DataStore.directBootAware) DirectBoot.update()
- finish()
- }
- val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.profile_config_menu, menu)
- return true
- }
- override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
- override fun onBackPressed() {
- if (needSave()) {
- UnsavedChangesDialogFragment().apply { key() }
- .show(supportFragmentManager, null)
- } else super.onBackPressed()
- }
- override fun onSupportNavigateUp(): Boolean {
- if (!super.onSupportNavigateUp()) finish()
- return true
- }
- override fun onDestroy() {
- DataStore.profileCacheStore.unregisterChangeListener(this)
- super.onDestroy()
- }
- override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
- if (key != Key.PROFILE_DIRTY) {
- DataStore.dirty = true
- }
- }
- class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {
- lateinit var activity: RouteSettingsActivity
- override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
- preferenceManager.preferenceDataStore = DataStore.profileCacheStore
- activity.apply {
- createPreferences(savedInstanceState, rootKey)
- }
- }
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)
- activity.apply {
- viewCreated(view, savedInstanceState)
- }
- }
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.action_delete -> {
- if (DataStore.editingId == 0L) {
- requireActivity().finish()
- } else {
- DeleteConfirmationDialogFragment().apply {
- arg(ProfileIdArg(DataStore.editingId))
- key()
- }.show(parentFragmentManager, null)
- }
- true
- }
- R.id.action_apply -> {
- runOnDefaultDispatcher {
- activity.saveAndExit()
- }
- true
- }
- else -> false
- }
- override fun onDisplayPreferenceDialog(preference: Preference) {
- activity.apply {
- if (displayPreferenceDialog(preference)) return
- }
- super.onDisplayPreferenceDialog(preference)
- }
- }
- object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
- override fun provideSummary(preference: EditTextPreference): CharSequence {
- return if (preference.text.isNullOrBlank()) {
- preference.context.getString(androidx.preference.R.string.not_set)
- } else {
- "\u2022".repeat(preference.text.length)
- }
- }
- }
- }
|