Unverified Commit 0055e53a authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1892 from Mygod/json

Add import/export to JSON
parents 31fdcfaf ca386a75
...@@ -27,7 +27,7 @@ import org.junit.Test ...@@ -27,7 +27,7 @@ import org.junit.Test
class ProfileTest { class ProfileTest {
@Test @Test
fun parsing() { fun parsing() {
val results = Profile.findAll("garble ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server garble") val results = Profile.findAllUrls("garble ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server garble")
.toList() .toList()
Assert.assertEquals(1, results.size) Assert.assertEquals(1, results.size)
Assert.assertEquals("ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888#example-server".toUri(), Assert.assertEquals("ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888#example-server".toUri(),
......
...@@ -223,13 +223,13 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener { ...@@ -223,13 +223,13 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
} else Snackbar.make(appListView, R.string.action_export_err, Snackbar.LENGTH_LONG).show() } else Snackbar.make(appListView, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
return true return true
} }
R.id.action_export -> { R.id.action_export_clipboard -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual, clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.individual}") "${DataStore.bypass}\n${DataStore.individual}")
Snackbar.make(appListView, R.string.action_export_msg, Snackbar.LENGTH_LONG).show() Snackbar.make(appListView, R.string.action_export_msg, Snackbar.LENGTH_LONG).show()
return true return true
} }
R.id.action_import -> { R.id.action_import_clipboard -> {
val proxiedAppString = clipboard.primaryClip?.getItemAt(0)?.text?.toString() val proxiedAppString = clipboard.primaryClip?.getItemAt(0)?.text?.toString()
if (!proxiedAppString.isNullOrEmpty()) { if (!proxiedAppString.isNullOrEmpty()) {
val i = proxiedAppString!!.indexOf('\n') val i = proxiedAppString!!.indexOf('\n')
......
...@@ -200,7 +200,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -200,7 +200,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) app.startService() else { if (requestCode != REQUEST_CONNECT) super.onActivityResult(requestCode, resultCode, data)
else if (resultCode == Activity.RESULT_OK) app.startService() else {
snackbar().setText(R.string.vpn_permission_denied).show() snackbar().setText(R.string.vpn_permission_denied).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
} }
...@@ -272,7 +273,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre ...@@ -272,7 +273,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
else -> null else -> null
} }
if (sharedStr.isNullOrEmpty()) return if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAll(sharedStr).toList() val profiles = Profile.findAllUrls(sharedStr, app.currentProfile).toList()
if (profiles.isEmpty()) { if (profiles.isEmpty()) {
snackbar().setText(R.string.profile_invalid_input).show() snackbar().setText(R.string.profile_invalid_input).show()
return return
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
...@@ -48,11 +49,14 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -48,11 +49,14 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.datas
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView import com.google.android.gms.ads.AdView
import net.glxn.qrgen.android.QRCode import net.glxn.qrgen.android.QRCode
import org.json.JSONArray
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
companion object { companion object {
...@@ -62,6 +66,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -62,6 +66,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
var instance: ProfilesFragment? = null var instance: ProfilesFragment? = null
private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL" private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
private const val REQUEST_IMPORT_PROFILES = 1
private const val REQUEST_EXPORT_PROFILES = 2
} }
/** /**
...@@ -210,7 +216,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -210,7 +216,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
.commitAllowingStateLoss() .commitAllowingStateLoss()
true true
} }
R.id.action_export -> { R.id.action_export_clipboard -> {
clipboard.primaryClip = ClipData.newPlainText(null, this.item.toString()) clipboard.primaryClip = ClipData.newPlainText(null, this.item.toString())
true true
} }
...@@ -363,9 +369,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -363,9 +369,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
startActivity(Intent(context, ScannerActivity::class.java)) startActivity(Intent(context, ScannerActivity::class.java))
true true
} }
R.id.action_import -> { R.id.action_import_clipboard -> {
try { try {
val profiles = Profile.findAll(clipboard.primaryClip!!.getItemAt(0).text).toList() val profiles = Profile.findAllUrls(clipboard.primaryClip!!.getItemAt(0).text, app.currentProfile)
.toList()
if (profiles.isNotEmpty()) { if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) } profiles.forEach { ProfileManager.createProfile(it) }
(activity as MainActivity).snackbar().setText(R.string.action_import_msg).show() (activity as MainActivity).snackbar().setText(R.string.action_import_msg).show()
...@@ -377,11 +384,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -377,11 +384,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
(activity as MainActivity).snackbar().setText(R.string.action_import_err).show() (activity as MainActivity).snackbar().setText(R.string.action_import_err).show()
true true
} }
R.id.action_import_file -> {
startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}, REQUEST_IMPORT_PROFILES)
true
}
R.id.action_manual_settings -> { R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile()) startConfig(ProfileManager.createProfile(
Profile().also { app.currentProfile?.copyFeatureSettingsTo(it) }))
true true
} }
R.id.action_export -> { R.id.action_export_clipboard -> {
val profiles = ProfileManager.getAllProfiles() val profiles = ProfileManager.getAllProfiles()
(activity as MainActivity).snackbar().setText(if (profiles != null) { (activity as MainActivity).snackbar().setText(if (profiles != null) {
clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n")) clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n"))
...@@ -389,10 +405,52 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -389,10 +405,52 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} else R.string.action_export_err).show() } else R.string.action_export_err).show()
true true
} }
R.id.action_export_file -> {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_TITLE, "profiles.json") // optional title that can be edited
}, REQUEST_EXPORT_PROFILES)
true
}
else -> false else -> false
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode != Activity.RESULT_OK) super.onActivityResult(requestCode, resultCode, data)
else when (requestCode) {
REQUEST_IMPORT_PROFILES -> {
val feature = app.currentProfile
var success = false
val activity = activity as MainActivity
for (uri in data!!.datas) try {
Profile.parseJson(activity.contentResolver.openInputStream(uri).bufferedReader().readText(),
feature).forEach {
ProfileManager.createProfile(it)
success = true
}
} catch (e: Exception) {
printLog(e)
}
activity.snackbar().setText(if (success) R.string.action_import_msg else R.string.action_import_err)
.show()
}
REQUEST_EXPORT_PROFILES -> {
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) try {
requireContext().contentResolver.openOutputStream(data!!.data).bufferedWriter().use {
it.write(JSONArray(profiles.map { it.toJson() }.toTypedArray()).toString(2))
}
} catch (e: Exception) {
printLog(e)
(activity as MainActivity).snackbar(e.localizedMessage).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { override fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
if (profileId != -1L) { // ignore resets from MainActivity if (profileId != -1L) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) { if (bandwidthProfile != profileId) {
......
...@@ -35,9 +35,12 @@ import androidx.appcompat.app.AppCompatActivity ...@@ -35,9 +35,12 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.util.forEach
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.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.utils.datas
import com.github.shadowsocks.utils.openBitmap import com.github.shadowsocks.utils.openBitmap
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
...@@ -107,7 +110,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -107,7 +110,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
} }
override fun onRetrieved(barcode: Barcode) = runOnUiThread { override fun onRetrieved(barcode: Barcode) = runOnUiThread {
Profile.findAll(barcode.rawValue).forEach { ProfileManager.createProfile(it) } Profile.findAllUrls(barcode.rawValue, app.currentProfile).forEach { ProfileManager.createProfile(it) }
navigateUp() navigateUp()
} }
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false) override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
...@@ -119,31 +122,31 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba ...@@ -119,31 +122,31 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
} }
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) { override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
R.id.action_import -> { R.id.action_import_clipboard -> {
startImport(true) startImport(true)
true true
} }
else -> false else -> false
} }
private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT) private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
.addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*") type = "image/*"
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true), if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}, if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) { REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
val feature = app.currentProfile
var success = false var success = false
var list = listOfNotNull(data?.data) for (uri in data!!.datas) try {
val clipData = data?.clipData detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build())
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri } .forEach { _, barcode ->
for (uri in list) try { Profile.findAllUrls(barcode.rawValue, feature).forEach {
val barcodes = detector.detect(Frame.Builder() ProfileManager.createProfile(it)
.setBitmap(contentResolver.openBitmap(uri)).build()) success = true
for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach { }
ProfileManager.createProfile(it) }
success = true
}
} catch (e: Exception) { } catch (e: Exception) {
printLog(e) printLog(e)
} }
......
...@@ -420,7 +420,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -420,7 +420,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
dialog.show() dialog.show()
true true
} }
R.id.action_import -> { R.id.action_import_clipboard -> {
try { try {
check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null) check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null)
} catch (exc: Exception) { } catch (exc: Exception) {
......
...@@ -47,6 +47,9 @@ import com.github.shadowsocks.plugin.PluginOptions ...@@ -47,6 +47,9 @@ import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.* import com.github.shadowsocks.utils.*
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
...@@ -54,10 +57,6 @@ import java.net.UnknownHostException ...@@ -54,10 +57,6 @@ import java.net.UnknownHostException
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
/** /**
* This object uses WeakMap to simulate the effects of multi-inheritance. * This object uses WeakMap to simulate the effects of multi-inheritance.
...@@ -186,11 +185,7 @@ object BaseService { ...@@ -186,11 +185,7 @@ object BaseService {
internal var shadowsocksConfigFile: File? = null internal var shadowsocksConfigFile: File? = null
internal fun buildShadowsocksConfig(): File { internal fun buildShadowsocksConfig(): File {
val profile = profile!! val profile = profile!!
val config = JSONObject() val config = profile.toJson(true)
.put("server", profile.host)
.put("server_port", profile.remotePort)
.put("password", profile.password)
.put("method", profile.method)
val pluginPath = pluginPath val pluginPath = pluginPath
if (pluginPath != null) { if (pluginPath != null) {
val pluginCmd = arrayListOf(pluginPath) val pluginCmd = arrayListOf(pluginPath)
...@@ -200,12 +195,12 @@ object BaseService { ...@@ -200,12 +195,12 @@ object BaseService {
.put("plugin_opts", plugin.toString()) .put("plugin_opts", plugin.toString())
} }
// sensitive Shadowsocks config is stored in // sensitive Shadowsocks config is stored in
val file = File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) { return File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
app.deviceStorage.noBackupFilesDir // only API 24+ will be in locked state app.deviceStorage.noBackupFilesDir // only API 24+ will be in locked state
}, CONFIG_FILE) }, CONFIG_FILE).apply {
shadowsocksConfigFile = file shadowsocksConfigFile = this
file.writeText(config.toString()) writeText(config.toString())
return file }
} }
val aclFile: File? get() { val aclFile: File? get() {
......
...@@ -26,9 +26,14 @@ import android.util.Log ...@@ -26,9 +26,14 @@ import android.util.Log
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.room.* import androidx.room.*
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginOptions
import com.github.shadowsocks.preference.DataStore 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.parsePort import com.github.shadowsocks.utils.parsePort
import org.json.JSONArray
import org.json.JSONObject
import org.json.JSONTokener
import java.io.Serializable import java.io.Serializable
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
...@@ -43,13 +48,14 @@ class Profile : Serializable { ...@@ -43,13 +48,14 @@ class Profile : Serializable {
private val userInfoPattern = "^(.+?):(.*)$".toRegex() private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex() private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map { fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data ?: "").map {
val uri = it.value.toUri() val uri = it.value.toUri()
try { try {
if (uri.userInfo == null) { if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING))) val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) { if (match != null) {
val profile = Profile() val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase() profile.method = match.groupValues[1].toLowerCase()
profile.password = match.groupValues[2] profile.password = match.groupValues[2]
profile.host = match.groupValues[3] profile.host = match.groupValues[3]
...@@ -66,6 +72,7 @@ class Profile : Serializable { ...@@ -66,6 +72,7 @@ class Profile : Serializable {
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE))) Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) { if (match != null) {
val profile = Profile() val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1] profile.method = match.groupValues[1]
profile.password = match.groupValues[2] profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855 // bug in Android: https://code.google.com/p/android/issues/detail?id=192855
...@@ -92,6 +99,54 @@ class Profile : Serializable { ...@@ -92,6 +99,54 @@ class Profile : Serializable {
null null
} }
}.filterNotNull() }.filterNotNull()
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
private fun tryAdd(json: JSONObject) {
val host = json.optString("server")
if (host.isNullOrEmpty()) return
val remotePort = json.optInt("server_port")
if (remotePort <= 0) return
val password = json.optString("password")
if (password.isNullOrEmpty()) return
val method = json.optString("method")
if (method.isNullOrEmpty()) return
add(Profile().also {
it.host = host
it.remotePort = remotePort
it.password = password
it.method = method
}.apply {
feature?.copyFeatureSettingsTo(this)
val id = json.optString("plugin")
if (!id.isNullOrEmpty()) {
plugin = PluginOptions(id, json.optString("plugin_opts")).toString(false)
}
name = json.optString("remarks")
route = json.optString("route", route)
remoteDns = json.optString("remote_dns", remoteDns)
ipv6 = json.optBoolean("ipv6", ipv6)
json.optJSONObject("proxy_apps")?.also {
proxyApps = it.optBoolean("enabled", proxyApps)
bypass = it.optBoolean("bypass", bypass)
individual = it.optJSONArray("android_list")?.asIterable()?.joinToString("\n") ?: individual
}
udpdns = json.optBoolean("udpdns", udpdns)
})
}
fun process(json: Any) {
when (json) {
is JSONObject -> {
tryAdd(json)
for (key in json.keys()) process(json.get(key))
}
is JSONArray -> json.asIterable().forEach(this::process)
// ignore other types
}
}
}
fun parseJson(json: String, feature: Profile? = null): List<Profile> =
JsonParser(feature).apply { process(JSONTokener(json).nextValue()) }
} }
@androidx.room.Dao @androidx.room.Dao
...@@ -143,6 +198,15 @@ class Profile : Serializable { ...@@ -143,6 +198,15 @@ class Profile : Serializable {
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!!
fun copyFeatureSettingsTo(profile: Profile) {
profile.route = route
profile.ipv6 = ipv6
profile.proxyApps = proxyApps
profile.bypass = bypass
profile.individual = individual
profile.udpdns = udpdns
}
fun toUri(): Uri { fun toUri(): Uri {
val builder = Uri.Builder() val builder = Uri.Builder()
.scheme("ss") .scheme("ss")
...@@ -158,6 +222,33 @@ class Profile : Serializable { ...@@ -158,6 +222,33 @@ class Profile : Serializable {
} }
override fun toString() = toUri().toString() override fun toString() = toUri().toString()
fun toJson(compat: Boolean = false) = JSONObject().apply {
put("server", host)
put("server_port", remotePort)
put("password", password)
put("method", method)
if (compat) return@apply
PluginConfiguration(plugin ?: "").selectedOptions.apply {
if (id.isNotEmpty()) {
put("plugin", id)
put("plugin_opts", toString())
}
}
put("remarks", name)
put("route", route)
put("remote_dns", remoteDns)
put("ipv6", ipv6)
put("proxy_apps", JSONObject().apply {
put("enabled", proxyApps)
if (proxyApps) {
put("bypass", bypass)
// android_ prefix is used because package names are Android specific
put("android_list", JSONArray(individual.split("\n")))
}
})
put("udpdns", udpdns)
}
fun serialize() { fun serialize() {
DataStore.privateStore.putString(Key.name, name) DataStore.privateStore.putString(Key.name, name)
DataStore.privateStore.putString(Key.host, host) DataStore.privateStore.putString(Key.host, host)
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.database.sqlite.SQLiteCantOpenDatabaseException import android.database.sqlite.SQLiteCantOpenDatabaseException
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.ProfilesFragment import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
...@@ -35,19 +34,8 @@ import java.sql.SQLException ...@@ -35,19 +34,8 @@ import java.sql.SQLException
*/ */
object ProfileManager { object ProfileManager {
@Throws(SQLException::class) @Throws(SQLException::class)
fun createProfile(p: Profile? = null): Profile { fun createProfile(profile: Profile = Profile()): Profile {
val profile = p ?: Profile()
profile.id = 0 profile.id = 0
val oldProfile = app.currentProfile
if (oldProfile != null) {
// Copy Feature Settings from old profile
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
}
profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0 profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0
profile.id = PrivateDatabase.profileDao.create(profile) profile.id = PrivateDatabase.profileDao.create(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile) ProfilesFragment.instance?.profilesAdapter?.add(profile)
......
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 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.utils
import android.content.ClipData
import androidx.recyclerview.widget.SortedList
import org.json.JSONArray
private sealed class ArrayIterator<out T> : Iterator<T> {
abstract val size: Int
abstract operator fun get(index: Int): T
private var count = 0
override fun hasNext() = count < size
override fun next(): T = if (hasNext()) this[count++] else throw NoSuchElementException()
}
private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipData.Item>() {
override val size get() = data.itemCount
override fun get(index: Int) = data.getItemAt(index)
}
fun ClipData.asIterable() = Iterable { ClipDataIterator(this) }
private class JSONArrayIterator(private val arr: JSONArray) : ArrayIterator<Any>() {
override val size get() = arr.length()
override fun get(index: Int) = arr.get(index)
}
fun JSONArray.asIterable() = Iterable { JSONArrayIterator(this) }
private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
override val size get() = list.size()
override fun get(index: Int) = list[index]
}
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) }
...@@ -12,7 +12,6 @@ import android.net.Uri ...@@ -12,7 +12,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.recyclerview.widget.SortedList
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
import java.net.InetAddress import java.net.InetAddress
...@@ -63,15 +62,7 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int { ...@@ -63,15 +62,7 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
return typedValue.resourceId return typedValue.resourceId
} }
private class SortedListIterable<out T>(private val list: SortedList<T>) : Iterable<T> { val Intent.datas get() = listOfNotNull(data) + (clipData?.asIterable()?.map { it.uri }?.filterNotNull() ?: emptyList())
override fun iterator(): Iterator<T> = SortedListIterator(list)
}
private class SortedListIterator<out T>(private val list: SortedList<T>) : Iterator<T> {
private var count = 0
override fun hasNext() = count < list.size()
override fun next(): T = if (hasNext()) list[count++] else throw NoSuchElementException()
}
fun <T> SortedList<T>.asIterable(): Iterable<T> = SortedListIterable(this)
fun printLog(t: Throwable) { fun printLog(t: Throwable) {
Crashlytics.logException(t) Crashlytics.logException(t)
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>
...@@ -7,14 +7,14 @@ ...@@ -7,14 +7,14 @@
android:numericShortcut="1" android:numericShortcut="1"
app:showAsAction="never"/> app:showAsAction="never"/>
<item <item
android:id="@+id/action_export" android:id="@+id/action_export_clipboard"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
android:icon="?attr/actionModeCopyDrawable" android:icon="?attr/actionModeCopyDrawable"
android:numericShortcut="2" android:numericShortcut="2"
android:title="@string/action_export" android:title="@string/action_export"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_import" android:id="@+id/action_import_clipboard"
android:alphabeticShortcut="i" android:alphabeticShortcut="i"
android:icon="?attr/actionModePasteDrawable" android:icon="?attr/actionModePasteDrawable"
android:numericShortcut="3" android:numericShortcut="3"
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
android:title="@string/add_profile_methods_manual_settings" android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m" android:alphabeticShortcut="m"
android:numericShortcut="1"/> android:numericShortcut="1"/>
<item android:id="@+id/action_import" <item android:id="@+id/action_import_clipboard"
android:title="@string/action_import" android:title="@string/action_import"
android:alphabeticShortcut="i" android:alphabeticShortcut="i"
android:numericShortcut="2"/> android:numericShortcut="2"/>
......
...@@ -14,22 +14,39 @@ ...@@ -14,22 +14,39 @@
android:numericShortcut="1" android:numericShortcut="1"
android:title="@string/add_profile_methods_scan_qr_code"/> android:title="@string/add_profile_methods_scan_qr_code"/>
<item <item
android:id="@+id/action_import" android:id="@+id/action_import_clipboard"
android:alphabeticShortcut="i" android:alphabeticShortcut="c"
android:numericShortcut="2" android:numericShortcut="2"
android:title="@string/action_import"/> android:title="@string/action_import"/>
<item
android:id="@+id/action_import_file"
android:alphabeticShortcut="f"
android:numericShortcut="3"
android:title="@string/action_import_file"/>
<item <item
android:id="@+id/action_manual_settings" android:id="@+id/action_manual_settings"
android:alphabeticShortcut="m" android:alphabeticShortcut="m"
android:numericShortcut="3" android:numericShortcut="4"
android:title="@string/add_profile_methods_manual_settings"/> android:title="@string/add_profile_methods_manual_settings"/>
</menu> </menu>
</item> </item>
<item <item
android:id="@+id/action_export"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
android:icon="?attr/actionModeCopyDrawable" android:icon="@drawable/ic_file_file_upload"
android:numericShortcut="1" android:numericShortcut="1"
android:title="@string/action_export" android:title="@string/action_export_more"
app:showAsAction="always"/> app:showAsAction="always">
<menu>
<item
android:id="@+id/action_export_clipboard"
android:alphabeticShortcut="c"
android:numericShortcut="1"
android:title="@string/action_export"/>
<item
android:id="@+id/action_export_file"
android:alphabeticShortcut="f"
android:numericShortcut="2"
android:title="@string/action_export_file"/>
</menu>
</item>
</menu> </menu>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
android:title="@string/share_qr_nfc" android:title="@string/share_qr_nfc"
android:alphabeticShortcut="q" android:alphabeticShortcut="q"
android:numericShortcut="1"/> android:numericShortcut="1"/>
<item android:id="@+id/action_export" <item android:id="@+id/action_export_clipboard"
android:title="@string/action_export" android:title="@string/action_export"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
android:numericShortcut="2"/> android:numericShortcut="2"/>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_import" <item android:id="@+id/action_import_clipboard"
android:title="@string/action_import_file" android:title="@string/action_import_file"
android:icon="@drawable/ic_image_photo" android:icon="@drawable/ic_image_photo"
android:alphabeticShortcut="p" android:alphabeticShortcut="p"
......
...@@ -93,6 +93,8 @@ ...@@ -93,6 +93,8 @@
<string name="share">Share</string> <string name="share">Share</string>
<string name="add_profile">Add Profile</string> <string name="add_profile">Add Profile</string>
<string name="action_apply_all">Apply Settings to All Profiles</string> <string name="action_apply_all">Apply Settings to All Profiles</string>
<string name="action_export_more">Export…</string>
<string name="action_export_file">Export to file…</string>
<string name="action_export">Export to Clipboard</string> <string name="action_export">Export to Clipboard</string>
<string name="action_import">Import from Clipboard</string> <string name="action_import">Import from Clipboard</string>
<string name="action_import_file">Import from file…</string> <string name="action_import_file">Import from file…</string>
......
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