diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..abdf814 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,28 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: shadowsocks/shadowsocks-android:circleci + environment: + JVM_OPTS: -Xmx3500m + GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process" + steps: + - checkout + - run: git submodule update --init --recursive + - restore_cache: + key: jars-{{ checksum "build.gradle" }} + - run: + name: Run Build and Tests + command: ./gradlew assembleDebug check + - save_cache: + paths: + - ~/.gradle + - ~/.android/build-cache + key: jars-{{ checksum "build.gradle" }} + - store_artifacts: + path: app/build/outputs/apk + destination: apk + - store_artifacts: + path: app/build/reports + destination: reports diff --git a/README.md b/README.md index ea28ea8..661ecc8 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ This plugin is an official plugin thus you can see [shadowsocks-android](https:/ ### LICENSE -Copyright (C) 2017 by Max Lv <> -Copyright (C) 2017 by Mygod Studio <> +Copyright (C) 2019 by Max Lv <> +Copyright (C) 2019 by Mygod Studio <> 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 diff --git a/app/build.gradle b/app/build.gradle index 04aa8c4..5ae6fd3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,8 +71,10 @@ tasks.whenTaskAdded { task -> dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "androidx.preference:preference:1.1.0-alpha02" implementation 'com.github.shadowsocks:plugin:1.0.0' + implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8ed9d0f..7c75d70 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,13 +22,10 @@ - - + - + * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 com.github.shadowsocks.plugin.v2ray import android.net.Uri diff --git a/app/src/main/java/com/github/shadowsocks/plugin/v2ray/CertificatePreferenceDialogFragment.kt b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/CertificatePreferenceDialogFragment.kt new file mode 100644 index 0000000..a1a18b1 --- /dev/null +++ b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/CertificatePreferenceDialogFragment.kt @@ -0,0 +1,52 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 com.github.shadowsocks.plugin.v2ray + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.preference.EditTextPreferenceDialogFragmentCompat +import com.google.android.material.snackbar.Snackbar + +class CertificatePreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() { + fun setKey(key: String) { + arguments = bundleOf(Pair(ARG_KEY, key)) + } + + override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { + super.onPrepareDialogBuilder(builder) + builder.setNeutralButton("Browse…") { _, _ -> + val activity = requireActivity() + try { + targetFragment!!.startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/pkix-cert" + }, ConfigFragment.REQUEST_BROWSE_CERTIFICATE) + return@setNeutralButton + } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } + Snackbar.make(activity.findViewById(R.id.content), + "Please install a file manager like MiXplorer", + Snackbar.LENGTH_SHORT).show() + } + } +} diff --git a/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigActivity.kt b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigActivity.kt new file mode 100644 index 0000000..0d523b9 --- /dev/null +++ b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigActivity.kt @@ -0,0 +1,69 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 com.github.shadowsocks.plugin.v2ray + +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import com.github.shadowsocks.plugin.ConfigurationActivity +import com.github.shadowsocks.plugin.PluginOptions + +class ConfigActivity : ConfigurationActivity(), Toolbar.OnMenuItemClickListener { + private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ConfigFragment } + private lateinit var oldOptions: PluginOptions + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + findViewById(R.id.toolbar).apply { + title = this@ConfigActivity.title + setNavigationIcon(R.drawable.ic_navigation_close) + setNavigationOnClickListener { onBackPressed() } + inflateMenu(R.menu.toolbar_config) + setOnMenuItemClickListener(this@ConfigActivity) + } + } + + override fun onInitializePluginOptions(options: PluginOptions) { + oldOptions = options + child.onInitializePluginOptions(options) + } + + override fun onMenuItemClick(item: MenuItem?) = when (item?.itemId) { + R.id.action_apply -> { + saveChanges(child.options) + finish() + true + } + else -> false + } + + override fun onBackPressed() { + if (child.options != oldOptions) AlertDialog.Builder(this).run { + setTitle(R.string.unsaved_changes_prompt) + setPositiveButton(R.string.yes) { _, _ -> saveChanges(child.options) } + setNegativeButton(R.string.no) { _, _ -> finish() } + setNeutralButton(android.R.string.cancel, null) + create() + }.show() else super.onBackPressed() + } +} diff --git a/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigFragment.kt b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigFragment.kt new file mode 100644 index 0000000..bbe2d9c --- /dev/null +++ b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/ConfigFragment.kt @@ -0,0 +1,115 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 com.github.shadowsocks.plugin.v2ray + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.github.shadowsocks.plugin.PluginOptions +import com.google.android.material.snackbar.Snackbar +import java.lang.RuntimeException + +class ConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { + companion object { + const val REQUEST_BROWSE_CERTIFICATE = 1 + } + + private val mode by lazy { findPreference("mode") } + private val host by lazy { findPreference("host") } + private val path by lazy { findPreference("path") } + private val certRaw by lazy { findPreference("certRaw") } + + // todo: remove me for updated plugin lib + private fun PluginOptions.putWithDefault(key: String, value: String?, default: String? = null) = + if (value == null || value == default) remove(key) else put(key, value) + + private fun readMode(value: String = mode.value) = when (value) { + "websocket-http" -> Pair(null, null) + "websocket-tls" -> Pair(null, "") + "quic-tls" -> Pair("quic", null) + else -> { + check(false) + Pair(null, null) + } + } + + val options get() = PluginOptions().apply { + val (mode, tls) = readMode() + putWithDefault("mode", mode) + putWithDefault("tls", tls) + putWithDefault("host", host.text, "cloudfront.com") + putWithDefault("path", path.text, "/") + putWithDefault("certRaw", certRaw.text.replace("\n", ""), "") + } + + fun onInitializePluginOptions(options: PluginOptions) { + mode.value = when { + options["mode"] ?: "websocket" == "quic" -> "quic-tls" + options["tls"] != null -> "websocket-tls" + else -> "websocket-http" + }.also { onPreferenceChange(null, it) } + host.text = options["host"] ?: "cloudfront.com" + path.text = options["path"] ?: "/" + certRaw.text = options["certRaw"] + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.config) + mode.onPreferenceChangeListener = this + } + + override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { + val (mode, tls) = readMode(newValue as String) + path.isEnabled = mode == null + certRaw.isEnabled = mode != null || tls != null + return true + } + + override fun onDisplayPreferenceDialog(preference: Preference?) { + if (preference == certRaw) CertificatePreferenceDialogFragment().apply { + setKey(certRaw.key) + setTargetFragment(this@ConfigFragment, 0) + }.show(fragmentManager ?: return, certRaw.key) else super.onDisplayPreferenceDialog(preference) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + REQUEST_BROWSE_CERTIFICATE -> { + if (resultCode != Activity.RESULT_OK) return + val activity = requireActivity() + try { + // we read all its content here to avoid content URL permission issues + certRaw.text = activity.contentResolver.openInputStream(data!!.data!!)!! + .bufferedReader().readText() + } catch (e: RuntimeException) { + Snackbar.make(activity.findViewById(R.id.content), e.localizedMessage, Snackbar.LENGTH_LONG) + .show() + } + } + else -> super.onActivityResult(requestCode, resultCode, data) + } + } +} diff --git a/app/src/main/java/com/github/shadowsocks/plugin/v2ray/HelpCallback.kt b/app/src/main/java/com/github/shadowsocks/plugin/v2ray/HelpCallback.kt deleted file mode 100644 index afb33e0..0000000 --- a/app/src/main/java/com/github/shadowsocks/plugin/v2ray/HelpCallback.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.shadowsocks.plugin.v2ray - -import com.github.shadowsocks.plugin.PluginOptions - -class HelpCallback : com.github.shadowsocks.plugin.HelpCallback() { - override fun produceHelpMessage(options: PluginOptions): CharSequence = - """ - host=string - Host header for websocket. (default "cloudfront.com") - - mode=string - Transport mode: ws/quic. (default "ws") - - path=string - URL path for websocket. (default "/") - - security=string - Transport security: none/tls. (default "none") - - """.trimIndent() - -} diff --git a/app/src/main/res/drawable/ic_action_done.xml b/app/src/main/res/drawable/ic_action_done.xml new file mode 100644 index 0000000..99caef9 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..30383a2 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/menu/toolbar_config.xml b/app/src/main/res/menu/toolbar_config.xml new file mode 100644 index 0000000..2f6fb29 --- /dev/null +++ b/app/src/main/res/menu/toolbar_config.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..242edd2 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,8 @@ + + + + websocket-http + websocket-tls + quic-tls + + diff --git a/app/src/main/res/xml/config.xml b/app/src/main/res/xml/config.xml new file mode 100644 index 0000000..a1e0e84 --- /dev/null +++ b/app/src/main/res/xml/config.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/v2ray-plugin b/app/src/v2ray-plugin index f2d883c..b1add5f 160000 --- a/app/src/v2ray-plugin +++ b/app/src/v2ray-plugin @@ -1 +1 @@ -Subproject commit f2d883c91f2cc20afe19949462736e54adf90bbb +Subproject commit b1add5f9903bb717cc5ac42a9b8d55f55127128e