Merge pull request #2 from Mygod/master

Refine Android plugin
This commit is contained in:
Max Lv 2019-01-14 14:11:34 +08:00 committed by GitHub
commit 2eac270fef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 356 additions and 31 deletions

28
.circleci/config.yml Normal file
View 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

View file

@ -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

View file

@ -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'

View file

@ -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"

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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()
}

View 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>

View 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>

View 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>

View 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>

View 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