Unverified Commit f0141317 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #2123 from shadowsocks/plugin-1.2.0

Plugin library 1.2.0
parents 1c1a8ed9 f4621f3f
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
...@@ -38,6 +39,10 @@ android { ...@@ -38,6 +39,10 @@ android {
} }
} }
androidExtensions {
experimental = true
}
def lifecycleVersion = '2.0.0' def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0' def roomVersion = '2.0.0'
def workVersion = '1.0.0-rc01' def workVersion = '1.0.0-rc01'
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.net.Uri import android.net.Uri
import android.os.Parcelable
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.util.LongSparseArray import android.util.LongSparseArray
...@@ -32,6 +33,7 @@ import com.github.shadowsocks.preference.DataStore ...@@ -32,6 +33,7 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.asIterable import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.parsePort import com.github.shadowsocks.utils.parsePort
import kotlinx.android.parcel.Parcelize
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.json.JSONTokener import org.json.JSONTokener
...@@ -41,7 +43,31 @@ import java.net.URISyntaxException ...@@ -41,7 +43,31 @@ import java.net.URISyntaxException
import java.util.* import java.util.*
@Entity @Entity
class Profile : Serializable { @Parcelize
data class Profile(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var name: String? = "",
var host: String = "198.199.101.152",
var remotePort: Int = 8388,
var password: String = "u1rRWTssNv0p",
var method: String = "aes-256-cfb",
var route: String = "all",
var remoteDns: String = "8.8.8.8",
var proxyApps: Boolean = false,
var bypass: Boolean = false,
var udpdns: Boolean = false,
var ipv6: Boolean = true,
var individual: String = "",
var tx: Long = 0,
var rx: Long = 0,
var userOrder: Long = 0,
var plugin: String? = null,
var udpFallback: Long? = null,
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
) : Parcelable, Serializable {
companion object { companion object {
private const val TAG = "ShadowParser" private const val TAG = "ShadowParser"
private const val serialVersionUID = 0L private const val serialVersionUID = 0L
...@@ -200,29 +226,6 @@ class Profile : Serializable { ...@@ -200,29 +226,6 @@ class Profile : Serializable {
fun deleteAll(): Int fun deleteAll(): Int
} }
@PrimaryKey(autoGenerate = true)
var id: Long = 0
var name: String? = ""
var host: String = "198.199.101.152"
var remotePort: Int = 8388
var password: String = "u1rRWTssNv0p"
var method: String = "aes-256-cfb"
var route: String = "all"
var remoteDns: String = "8.8.8.8"
var proxyApps: Boolean = false
var bypass: Boolean = false
var udpdns: Boolean = false
var ipv6: Boolean = true
var individual: String = ""
var tx: Long = 0
var rx: Long = 0
var userOrder: Long = 0
var plugin: String? = null
var udpFallback: Long? = null
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort) val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!! val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
......
...@@ -5,6 +5,7 @@ import java.util.regex.Pattern ...@@ -5,6 +5,7 @@ import java.util.regex.Pattern
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def getCurrentFlavor() { def getCurrentFlavor() {
String task = getGradle().getStartParameter().getTaskRequests().toString() String task = getGradle().getStartParameter().getTaskRequests().toString()
...@@ -49,6 +50,10 @@ android { ...@@ -49,6 +50,10 @@ android {
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni") new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
} }
androidExtensions {
experimental = true
}
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "androidx.browser:browser:1.0.0" implementation "androidx.browser:browser:1.0.0"
......
...@@ -23,6 +23,7 @@ package com.github.shadowsocks ...@@ -23,6 +23,7 @@ package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.nfc.NdefMessage import android.nfc.NdefMessage
...@@ -30,6 +31,7 @@ import android.nfc.NfcAdapter ...@@ -30,6 +31,7 @@ import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import android.os.Parcelable
import android.util.Log import android.util.Log
import android.view.KeyCharacterMap import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
...@@ -51,6 +53,8 @@ import com.github.shadowsocks.aidl.TrafficStats ...@@ -51,6 +53,8 @@ import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
...@@ -58,6 +62,7 @@ import com.github.shadowsocks.widget.ServiceButton ...@@ -58,6 +62,7 @@ import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener, class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener { NavigationView.OnNavigationItemSelectedListener {
...@@ -68,6 +73,17 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -68,6 +73,17 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
var stateListener: ((Int) -> Unit)? = null var stateListener: ((Int) -> Unit)? = null
} }
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes) { _, _ -> arg.profiles.forEach { ProfileManager.createProfile(it) } }
setNegativeButton(R.string.no, null)
setMessage(arg.profiles.joinToString("\n"))
}
}
// UI // UI
private lateinit var fab: ServiceButton private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar private lateinit var stats: StatsBar
...@@ -188,17 +204,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -188,17 +204,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
} }
if (sharedStr.isNullOrEmpty()) return if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList() val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) { if (profiles.isEmpty()) snackbar().setText(R.string.profile_invalid_input).show()
snackbar().setText(R.string.profile_invalid_input).show() else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
return
}
AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(R.string.yes) { _, _ -> profiles.forEach { ProfileManager.createProfile(it) } }
.setNegativeButton(R.string.no, null)
.setMessage(profiles.joinToString("\n"))
.create()
.show()
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
......
...@@ -21,12 +21,15 @@ ...@@ -21,12 +21,15 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
...@@ -35,6 +38,15 @@ class ProfileConfigActivity : AppCompatActivity() { ...@@ -35,6 +38,15 @@ class ProfileConfigActivity : AppCompatActivity() {
const val REQUEST_CODE_PLUGIN_HELP = 1 const val REQUEST_CODE_PLUGIN_HELP = 1
} }
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.unsaved_changes_prompt)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setNeutralButton(android.R.string.cancel, null)
}
}
private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ProfileConfigFragment } private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ProfileConfigFragment }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
...@@ -59,13 +71,8 @@ class ProfileConfigActivity : AppCompatActivity() { ...@@ -59,13 +71,8 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item) override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
override fun onBackPressed() { override fun onBackPressed() {
if (DataStore.dirty) AlertDialog.Builder(this) if (DataStore.dirty) UnsavedChangesDialogFragment().show(child, ProfileConfigFragment.REQUEST_UNSAVED_CHANGES)
.setTitle(R.string.unsaved_changes_prompt) else super.onBackPressed()
.setPositiveButton(R.string.yes) { _, _ -> child.saveAndExit() }
.setNegativeButton(R.string.no) { _, _ -> finish() }
.setNeutralButton(android.R.string.cancel, null)
.create()
.show() else super.onBackPressed()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
...@@ -22,8 +22,10 @@ package com.github.shadowsocks ...@@ -22,8 +22,10 @@ package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
...@@ -33,10 +35,7 @@ import androidx.preference.SwitchPreference ...@@ -33,10 +35,7 @@ import androidx.preference.SwitchPreference
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
...@@ -47,11 +46,26 @@ import com.github.shadowsocks.utils.Key ...@@ -47,11 +46,26 @@ import com.github.shadowsocks.utils.Key
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.takisoft.preferencex.EditTextPreference import com.takisoft.preferencex.EditTextPreference
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
import kotlinx.android.parcel.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(), class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener { Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object { companion object {
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2
}
@Parcelize
data class ProfileIdArg(val profileId: Long) : Parcelable
class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.delete_confirm_prompt)
setPositiveButton(R.string.yes) { _, _ ->
ProfileManager.delProfile(arg.profileId)
requireActivity().finish()
}
setNegativeButton(R.string.no, null)
}
} }
private var profileId = -1L private var profileId = -1L
...@@ -116,7 +130,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -116,7 +130,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
bundleOf(Pair("key", Key.pluginConfigure), bundleOf(Pair("key", Key.pluginConfigure),
Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected))) Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected)))
fun saveAndExit() { private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile() val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId profile.id = profileId
profile.deserialize() profile.deserialize()
...@@ -161,28 +175,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -161,28 +175,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PLUGIN_CONFIGURE) when (resultCode) { when (requestCode) {
REQUEST_CODE_PLUGIN_CONFIGURE -> when (resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS) val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.text = options pluginConfigure.text = options
onPreferenceChange(null, options) onPreferenceChange(null, options)
} }
PluginContract.RESULT_FALLBACK -> showPluginEditor() PluginContract.RESULT_FALLBACK -> showPluginEditor()
} else super.onActivityResult(requestCode, resultCode, data) }
REQUEST_UNSAVED_CHANGES -> when (resultCode) {
DialogInterface.BUTTON_POSITIVE -> saveAndExit()
DialogInterface.BUTTON_NEGATIVE -> requireActivity().finish()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
val activity = requireActivity() DeleteConfirmationDialogFragment().withArg(ProfileIdArg(profileId)).show(this)
AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes) { _, _ ->
ProfileManager.delProfile(profileId)
activity.finish()
}
.setNegativeButton(R.string.no, null)
.create()
.show()
true true
} }
R.id.action_apply -> { R.id.action_apply -> {
......
...@@ -22,9 +22,12 @@ package com.github.shadowsocks.acl ...@@ -22,9 +22,12 @@ package com.github.shadowsocks.acl
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.* import android.view.*
...@@ -43,10 +46,12 @@ import com.github.shadowsocks.R ...@@ -43,10 +46,12 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.utils.asIterable import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.android.parcel.Parcelize
import java.net.IDN import java.net.IDN
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
...@@ -55,6 +60,8 @@ import java.util.regex.PatternSyntaxException ...@@ -55,6 +60,8 @@ import java.util.regex.PatternSyntaxException
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback { class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback {
companion object { companion object {
private const val REQUEST_CODE_ADD = 1
private const val REQUEST_CODE_EDIT = 2
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$" private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS" private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
...@@ -63,6 +70,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -63,6 +70,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
// unescaped: (?<=^(\(\^\|\\\.\)|\^\(\.\*\\\.\)\?)).*(?=\$$) // unescaped: (?<=^(\(\^\|\\\.\)|\^\(\.\*\\\.\)\?)).*(?=\$$)
private val domainPattern = "(?<=^(\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?)).*(?=\\\$\$)".toRegex() private val domainPattern = "(?<=^(\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?)).*(?=\\\$\$)".toRegex()
@Suppress("FunctionName")
private fun AclItem(item: Any) = when (item) {
is String -> AclItem(item, false)
is Subnet -> AclItem(item.toString(), false)
is URL -> AclItem(item.toString(), true)
else -> throw IllegalArgumentException("item")
}
} }
private enum class Template { private enum class Template {
...@@ -70,53 +85,50 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -70,53 +85,50 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
Domain, Domain,
Url; Url;
} }
private inner class AclRuleDialog(item: Any = "") : TextWatcher, AdapterView.OnItemSelectedListener { @Parcelize
val builder: AlertDialog.Builder data class AclItem(val item: String = "", val isUrl: Boolean = false) : Parcelable {
val templateSelector: Spinner fun toAny() = if (isUrl) URL(item) else Subnet.fromString(item) ?: item
val editText: EditText }
private val inputLayout: TextInputLayout @Parcelize
private lateinit var dialog: AlertDialog data class AclEditResult(val edited: AclItem, val replacing: AclItem) : Parcelable
class AclRuleDialogFragment : AlertDialogFragment<AclItem, AclEditResult>(),
TextWatcher, AdapterView.OnItemSelectedListener {
private lateinit var templateSelector: Spinner
private lateinit var editText: EditText
private lateinit var inputLayout: TextInputLayout
private lateinit var positive: Button private lateinit var positive: Button
init { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val activity = requireActivity() val activity = requireActivity()
val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null) val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
templateSelector = view.findViewById(R.id.template_selector) templateSelector = view.findViewById(R.id.template_selector)
editText = view.findViewById(R.id.content) editText = view.findViewById(R.id.content)
inputLayout = view.findViewById(R.id.content_layout) inputLayout = view.findViewById(R.id.content_layout)
when (item) { templateSelector.setSelection(Template.Generic.ordinal)
is String -> { editText.setText(arg.item)
val match = domainPattern.find(item) when {
arg.isUrl -> templateSelector.setSelection(Template.Url.ordinal)
Subnet.fromString(arg.item) == null -> {
val match = domainPattern.find(arg.item)
if (match != null) { if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal) templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."), editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)) IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
} else {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item)
}
} }
is URL -> {
templateSelector.setSelection(Template.Url.ordinal)
editText.setText(item.toString())
} }
else -> {
templateSelector.setSelection(Template.Generic.ordinal)
editText.setText(item.toString())
} }
} templateSelector.onItemSelectedListener = this@AclRuleDialogFragment
templateSelector.onItemSelectedListener = this editText.addTextChangedListener(this@AclRuleDialogFragment)
editText.addTextChangedListener(this) setTitle(R.string.edit_rule)
builder = AlertDialog.Builder(activity) setPositiveButton(android.R.string.ok, listener)
.setTitle(R.string.edit_rule) setNegativeButton(android.R.string.cancel, null)
.setNegativeButton(android.R.string.cancel, null) if (arg.item.isNotEmpty()) setNeutralButton(R.string.delete, listener)
.setView(view) setView(view)
} }
fun show() { override fun onStart() {
dialog = builder.create() super.onStart()
dialog.show() positive = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
validate() validate()
} }
...@@ -157,15 +169,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -157,15 +169,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout.error = message inputLayout.error = message
} }
fun add(): Int? { override val ret get() = AclEditResult(editText.text.toString().let { text ->
val text = editText.text.toString() when (Template.values()[templateSelector.selectedItemPosition]) {
return when (Template.values()[templateSelector.selectedItemPosition]) { Template.Generic -> AclItem(text)
Template.Generic -> adapter.addToProxy(text) Template.Domain -> AclItem(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
Template.Domain -> adapter.addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\."))) IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
Template.Url -> adapter.addURL(URL(text)) Template.Url -> AclItem(text, true)
}
} }
}, arg)
} }
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view), private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
...@@ -197,22 +208,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -197,22 +208,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
if (selectedItems.isNotEmpty()) onLongClick(v) else { if (selectedItems.isNotEmpty()) onLongClick(v)
val dialog = AclRuleDialog(item) else AclRuleDialogFragment().withArg(AclItem(item)).show(this@CustomRulesFragment, REQUEST_CODE_EDIT)
dialog.builder
.setNeutralButton(R.string.delete) { _, _ ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
}
.setPositiveButton(android.R.string.ok) { _, _ ->
adapter.remove(item)
val index = dialog.add() ?: adapter.add(item)
if (index != null) list.post { list.scrollToPosition(index) }
}
dialog.show()
} }
} override fun onLongClick(v: View?): Boolean {
override fun onLongClick(p0: View?): Boolean {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated() onSelectedItemsUpdated()
itemView.isSelected = !itemView.isSelected itemView.isSelected = !itemView.isSelected
...@@ -427,9 +426,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -427,9 +426,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_manual_settings -> { R.id.action_manual_settings -> {
val dialog = AclRuleDialog() AclRuleDialogFragment().withArg(AclItem()).show(this, REQUEST_CODE_ADD)
dialog.builder.setPositiveButton(android.R.string.ok) { _, _ -> dialog.add() }
dialog.show()
true true
} }
R.id.action_import_clipboard -> { R.id.action_import_clipboard -> {
...@@ -451,6 +448,25 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -451,6 +448,25 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
else -> false else -> false
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val editing = when (requestCode) {
REQUEST_CODE_ADD -> false
REQUEST_CODE_EDIT -> true
else -> return super.onActivityResult(requestCode, resultCode, data)
}
val ret by lazy { AlertDialogFragment.getRet<AclEditResult>(data!!) }
when (resultCode) {
DialogInterface.BUTTON_POSITIVE -> {
if (editing) adapter.remove(ret.replacing.toAny())
adapter.add(ret.edited.toAny())?.also { list.post { list.scrollToPosition(it) } }
}
DialogInterface.BUTTON_NEUTRAL -> ret.replacing.toAny().let { item ->
adapter.remove(item)
undoManager.remove(Pair(-1, item))
}
}
}
override fun onDetach() { override fun onDetach() {
undoManager.flush() undoManager.flush()
mode?.finish() mode?.finish()
......
* 1.2.0:
* New helper class `AlertDialogFragment` for creating `AlertDialog` that persists through configuration changes;
* Dependency update: `com.google.android.material:material:1.1.0-alpha03`.
* 1.1.0: * 1.1.0:
* Having control characters in plugin options is no longer allowed. * Having control characters in plugin options is no longer allowed.
If this breaks your plugin, you are doing it wrong. If this breaks your plugin, you are doing it wrong.
......
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: 'gradle-mvn-push.gradle' apply from: 'gradle-mvn-push.gradle'
...@@ -10,8 +11,8 @@ android { ...@@ -10,8 +11,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode 8 versionCode 9
versionName "1.1.0" versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
...@@ -23,6 +24,10 @@ android { ...@@ -23,6 +24,10 @@ android {
} }
} }
androidExtensions {
experimental = true
}
dependencies { dependencies {
api "androidx.core:core-ktx:1.0.1" api "androidx.core:core-ktx:1.0.1"
api "com.google.android.material:material:1.1.0-alpha03" api "com.google.android.material:material:1.1.0-alpha03"
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 com.github.shadowsocks.plugin
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.Fragment
import kotlinx.android.parcel.Parcelize
/**
* Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
*/
abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
AppCompatDialogFragment(), DialogInterface.OnClickListener {
companion object {
private const val KEY_ARG = "arg"
private const val KEY_RET = "ret"
fun <T : Parcelable> getRet(data: Intent) = data.extras!!.getParcelable<T>(KEY_RET)!!
}
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
protected val arg by lazy { arguments!!.getParcelable<Arg>(KEY_ARG)!! }
protected open val ret: Ret? get() = null
fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) {
targetFragment?.onActivityResult(targetRequestCode, which, ret?.let {
Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
})
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_CANCELED, null)
}
fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
setTargetFragment(target, requestCode)
show(target.fragmentManager ?: return, tag)
}
}
@Parcelize
class Empty : Parcelable
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment