commit
2eac270fef
15 changed files with 356 additions and 31 deletions
28
.circleci/config.yml
Normal file
28
.circleci/config.yml
Normal file
|
@ -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
|
|
@ -39,8 +39,8 @@ This plugin is an official plugin thus you can see [shadowsocks-android](https:/
|
|||
|
||||
### LICENSE
|
||||
|
||||
Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>>
|
||||
Copyright (C) 2017 by Mygod Studio <<contact-shadowsocks-android@mygod.be>>
|
||||
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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -22,13 +22,10 @@
|
|||
</intent-filter>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.id"
|
||||
android:value="v2ray"/>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
|
||||
android:value="host=test.example.com"/>
|
||||
</provider>
|
||||
<activity android:name=".HelpCallback"
|
||||
android:theme="@style/Theme.AppCompat.Translucent">
|
||||
<activity android:name=".ConfigActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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.v2ray
|
||||
|
||||
import android.net.Uri
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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.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<View>(R.id.content),
|
||||
"Please install a file manager like MiXplorer",
|
||||
Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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.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<Toolbar>(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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* 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.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<ListPreference>("mode") }
|
||||
private val host by lazy { findPreference<EditTextPreference>("host") }
|
||||
private val path by lazy { findPreference<EditTextPreference>("path") }
|
||||
private val certRaw by lazy { findPreference<EditTextPreference>("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<View>(R.id.content), e.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
9
app/src/main/res/drawable/ic_action_done.xml
Normal file
9
app/src/main/res/drawable/ic_action_done.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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="#FFFFFFFF"
|
||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||
</vector>
|
12
app/src/main/res/layout/activity_main.xml
Normal file
12
app/src/main/res/layout/activity_main.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<include layout="@layout/toolbar_light_dark" />
|
||||
<fragment android:id="@+id/content"
|
||||
class="com.github.shadowsocks.plugin.v2ray.ConfigFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
</LinearLayout>
|
10
app/src/main/res/menu/toolbar_config.xml
Normal file
10
app/src/main/res/menu/toolbar_config.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_apply"
|
||||
android:title="@string/apply"
|
||||
android:icon="@drawable/ic_action_done"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
8
app/src/main/res/values/arrays.xml
Normal file
8
app/src/main/res/values/arrays.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="modes">
|
||||
<item>websocket-http</item>
|
||||
<item>websocket-tls</item>
|
||||
<item>quic-tls</item>
|
||||
</string-array>
|
||||
</resources>
|
25
app/src/main/res/xml/config.xml
Normal file
25
app/src/main/res/xml/config.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<com.takisoft.preferencex.SimpleMenuPreference
|
||||
app:key="mode"
|
||||
app:persistent="false"
|
||||
app:entries="@array/modes"
|
||||
app:entryValues="@array/modes"
|
||||
app:title="Transport mode"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<EditTextPreference
|
||||
app:key="host"
|
||||
app:persistent="false"
|
||||
app:title="Hostname"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<EditTextPreference
|
||||
app:key="path"
|
||||
app:persistent="false"
|
||||
app:title="Path"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<EditTextPreference
|
||||
app:key="certRaw"
|
||||
app:persistent="false"
|
||||
app:title="Certificate for TLS verification"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
</PreferenceScreen>
|
|
@ -1 +1 @@
|
|||
Subproject commit f2d883c91f2cc20afe19949462736e54adf90bbb
|
||||
Subproject commit b1add5f9903bb717cc5ac42a9b8d55f55127128e
|
Loading…
Reference in a new issue