____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.ui.profile
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.annotation.LayoutRes
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceFragmentCompat
import com.esotericsoftware.kryo.io.ByteBufferInput
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.QuickToggleShortcut
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProfileManager
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.applyDefaultValues
import io.nekohasekai.sagernet.ktx.byteBuffer
import io.nekohasekai.sagernet.ktx.onMainDispatcher
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ui.ThemedActivity
import java.io.ByteArrayOutputStream
import kotlin.properties.Delegates
@Suppress("UNCHECKED_CAST")
abstract class ProfileSettingsActivity(
@LayoutRes resId: Int = R.layout.layout_config_settings,
) : ThemedActivity(resId),
OnPreferenceDataStoreChangeListener {
override val onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
MaterialAlertDialogBuilder(this@ProfileSettingsActivity)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(android.R.string.ok) { _, _ ->
runOnDefaultDispatcher {
saveAndExit()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
finish()
}
.show()
}
}
companion object {
const val EXTRA_PROFILE_ID = "id"
const val EXTRA_IS_SUBSCRIPTION = "sub"
const val KEY_TEMP_BEAN_BYTES = "temp_bean_bytes"
const val KEY_DIRTY = "dirty"
}
abstract fun createEntity(): T
abstract fun T.init()
abstract fun T.serialize()
protected var dirty = false
protected var bean: T? = null
protected var isSubscription by Delegates.notNull()
override fun onCreate(savedInstanceState: Bundle?) {
savedInstanceState?.getByteArray(KEY_TEMP_BEAN_BYTES)?.let {
bean = createEntity().apply {
deserialize(ByteBufferInput(it))
}
}
super.onCreate(savedInstanceState)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.apply {
setTitle(R.string.profile_config)
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_navigation_close)
}
if (savedInstanceState == null) runOnDefaultDispatcher {
val editingId = intent.getLongExtra(EXTRA_PROFILE_ID, 0L)
isSubscription = intent.getBooleanExtra(EXTRA_IS_SUBSCRIPTION, false)
DataStore.editingId = editingId
bean = if (editingId == 0L) {
DataStore.editingGroup = DataStore.selectedGroupForImport()
createEntity().applyDefaultValues()
} else {
val proxyEntity = SagerDatabase.proxyDao.getById(editingId)
if (proxyEntity == null) {
onMainDispatcher {
finish()
}
return@runOnDefaultDispatcher
}
DataStore.editingGroup = proxyEntity.groupId
(proxyEntity.requireBean() as T)
}
bean!!.init()
onMainDispatcher {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MyPreferenceFragmentCompat())
.commit()
}
}
savedInstanceState?.getBoolean(KEY_DIRTY)?.let {
dirty = it
onBackPressedCallback.isEnabled = it
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
bean?.let {
val output = ByteArrayOutputStream()
val buffer = output.byteBuffer()
it.serializeToBuffer(buffer)
buffer.flush()
buffer.close()
outState.putByteArray(KEY_TEMP_BEAN_BYTES, output.toByteArray())
}
outState.putBoolean(KEY_DIRTY, dirty)
}
open suspend fun saveAndExit() {
val editingId = DataStore.editingId
if (editingId == 0L) {
val editingGroup = DataStore.editingGroup
ProfileManager.createProfile(editingGroup, createEntity().apply { serialize() })
} else {
val entity = SagerDatabase.proxyDao.getById(DataStore.editingId)
if (entity == null) {
finish()
return
}
ProfileManager.updateProfile(entity.apply { (requireBean() as T).serialize() })
}
setResult(RESULT_OK)
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)
menu.findItem(R.id.action_create_shortcut)?.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && DataStore.editingId > 0L) {
isVisible = true
}
}
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> {
if (DataStore.editingId == 0L) {
finish()
} else {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(android.R.string.ok) { _, _ ->
runOnDefaultDispatcher {
ProfileManager.deleteProfile(DataStore.editingGroup, DataStore.editingId)
}
finish()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
true
}
R.id.action_apply -> {
runOnDefaultDispatcher {
saveAndExit()
}
true
}
R.id.action_create_shortcut -> {
val entity = SagerDatabase.proxyDao.getById(DataStore.editingId)
if (entity != null) {
val shortcut = ShortcutInfoCompat.Builder(this, "shortcut-profile-${entity.id}")
.setShortLabel(entity.displayName())
.setLongLabel(entity.displayName())
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_foreground))
.setIntent(Intent(this, QuickToggleShortcut::class.java).apply {
action = Intent.ACTION_MAIN
putExtra("profile", entity.id)
}
).build()
ShortcutManagerCompat.requestPinShortcut(this, shortcut, null)
}
true
}
else -> false
}
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) {
dirty = true
onBackPressedCallback.isEnabled = true
}
}
abstract fun PreferenceFragmentCompat.createPreferences(
savedInstanceState: Bundle?,
rootKey: String?,
)
open fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
}
open fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {
return false
}
class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {
val activity: ProfileSettingsActivity<*>
get() = requireActivity() as ProfileSettingsActivity<*>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.profileCacheStore
try {
activity.apply {
createPreferences(savedInstanceState, rootKey)
}
} catch (e: Exception) {
Logs.w(e)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = bars.left,
right = bars.right,
bottom = bars.bottom,
)
insets
}
activity.apply {
viewCreated(view, savedInstanceState)
DataStore.profileCacheStore.registerChangeListener(this)
}
}
override fun onDisplayPreferenceDialog(preference: Preference) {
activity.apply {
if (displayPreferenceDialog(preference)) return
}
super.onDisplayPreferenceDialog(preference)
}
}
object PasswordSummaryProvider : Preference.SummaryProvider {
override fun provideSummary(preference: EditTextPreference): CharSequence {
val text = preference.text
return if (text.isNullOrEmpty()) {
preference.context.getString(androidx.preference.R.string.not_set)
} else {
"\u2022".repeat(text.length)
}
}
}
}