├── shadow.png ├── .headache.cfg ├── lint.xml ├── shadow-notify.png ├── src └── main │ ├── ic_launcher-web.png │ ├── assets │ ├── fonts │ │ └── Iceland.ttf │ └── acl │ │ └── bypass-lan.acl │ ├── res │ ├── raw │ │ └── gtm_default_container │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ ├── drawable │ │ ├── background_header.png │ │ ├── background_stat.xml │ │ ├── ic_action_done.xml │ │ ├── background_selectable.xml │ │ ├── ic_action_delete.xml │ │ ├── ic_av_playlist_add.xml │ │ ├── ic_action_description.xml │ │ ├── ic_action_note_add.xml │ │ ├── ic_content_copy.xml │ │ ├── ic_image_edit.xml │ │ ├── ic_start_connected.xml │ │ ├── ic_navigation_refresh.xml │ │ ├── ic_content_paste.xml │ │ ├── ic_start_busy.xml │ │ ├── ic_action_assignment.xml │ │ ├── ic_action_help_outline.xml │ │ ├── ic_qu_shadowsocks_launcher.xml │ │ ├── ic_social_share.xml │ │ ├── ic_start_idle.xml │ │ ├── background_profile.xml │ │ ├── ic_action_settings.xml │ │ └── ic_action_copyright.xml │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ ├── values-v21 │ │ ├── dimen.xml │ │ ├── styles.xml │ │ └── strings.xml │ ├── drawable-hdpi │ │ ├── ic_navigation_close.png │ │ └── ic_stat_shadowsocks.png │ ├── drawable-mdpi │ │ ├── ic_navigation_close.png │ │ └── ic_stat_shadowsocks.png │ ├── drawable-xhdpi │ │ ├── ic_navigation_close.png │ │ └── ic_stat_shadowsocks.png │ ├── drawable-xxhdpi │ │ ├── ic_navigation_close.png │ │ └── ic_stat_shadowsocks.png │ ├── drawable-xxxhdpi │ │ └── ic_navigation_close.png │ ├── drawable-v21 │ │ ├── background_stat.xml │ │ └── background_selectable.xml │ ├── values │ │ ├── dimen.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── configs.xml │ │ └── styles.xml │ ├── drawable-anydpi-v21 │ │ └── ic_navigation_close.xml │ ├── menu │ │ ├── profile_share_popup.xml │ │ ├── profile_config_menu.xml │ │ ├── app_manager_menu.xml │ │ ├── profile_manager_menu.xml │ │ └── custom_rules_menu.xml │ ├── layout │ │ ├── layout_about.xml │ │ ├── toolbar_light_dark.xml │ │ ├── layout_global_settings.xml │ │ ├── layout_profile_config.xml │ │ ├── dialog_acl_rule.xml │ │ ├── layout_header.xml │ │ ├── layout_list.xml │ │ ├── layout_tasker.xml │ │ ├── layout_apps_item.xml │ │ ├── layout_custom_rules.xml │ │ ├── layout_apps.xml │ │ └── layout_profile.xml │ └── xml │ │ ├── shortcuts.xml │ │ ├── pref_global.xml │ │ └── tracker.xml │ ├── jni │ ├── Application.mk │ ├── include │ │ ├── sodium │ │ │ └── version.h │ │ └── libev │ │ │ └── config.h │ ├── build-shared-executable.mk │ └── system.cpp │ ├── aidl │ └── com │ │ └── github │ │ └── shadowsocks │ │ └── aidl │ │ ├── IShadowsocksServiceCallback.aidl │ │ └── IShadowsocksService.aidl │ ├── java │ └── com │ │ └── github │ │ └── shadowsocks │ │ ├── DialogFragment.java │ │ └── System.java │ └── scala │ ├── be │ └── mygod │ │ └── preference │ │ ├── DialogPreferencePlus.scala │ │ ├── PreferenceCategory.scala │ │ ├── SummaryPreference.scala │ │ ├── NumberPickerPreferenceDialogFragment.scala │ │ ├── EditTextPreferenceDialogFragment.scala │ │ ├── PreferenceFragment.scala │ │ ├── EditTextPreference.scala │ │ ├── NumberPickerPreference.scala │ │ └── PreferenceGroupAdapter.scala │ └── com │ └── github │ └── shadowsocks │ ├── acl │ ├── Subnet.scala │ ├── DonaldTrump.scala │ └── AclSyncJob.scala │ ├── widget │ ├── BoundedCardView.scala │ ├── BoundedGridLayout.scala │ ├── BoundedView.scala │ └── UndoSnackbarManager.scala │ ├── utils │ ├── IOUtils.scala │ ├── CloseUtils.scala │ ├── Typefaces.scala │ ├── TcpFastOpen.scala │ ├── TaskerSettings.scala │ ├── TrafficMonitor.scala │ ├── TrafficMonitorThread.scala │ └── Parser.scala │ ├── TaskerReceiver.scala │ ├── ToolbarFragment.scala │ ├── BootReceiver.scala │ ├── ShadowsocksRunnerService.scala │ ├── ShadowsocksBackupAgent.scala │ ├── AboutFragment.scala │ ├── GlobalSettingsFragment.scala │ ├── preferences │ └── KcpCliPreferenceDialogFragment.scala │ ├── QuickToggleShortcut.scala │ ├── ProfileConfigActivity.scala │ ├── GlobalConfigFragment.scala │ ├── QRCodeDialog.scala │ ├── ShadowsocksTileService.scala │ ├── ShadowsocksRunnerActivity.scala │ ├── GuardedProcess.scala │ ├── ShadowsocksVpnThread.scala │ └── database │ └── ProfileManager.scala ├── .jvmopts ├── CONTRIBUTING.md ├── local.properties.example ├── project └── plugins.sbt ├── .github ├── pull_request_template.md ├── issue_template.md └── faq.md ├── gfwlist ├── gen.pl └── gen.py ├── AUTHORS ├── .gitignore ├── LICENSE ├── .gitmodules ├── .travis.yml ├── kcptun └── make.bash └── README.md /shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/shadow.png -------------------------------------------------------------------------------- /.headache.cfg: -------------------------------------------------------------------------------- 1 | # Scala source 2 | ".*\\.scala" -> frame open:"/*" line:"*" close:"*/" 3 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shadow-notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/shadow-notify.png -------------------------------------------------------------------------------- /src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -XX:MaxPermSize=512m 2 | -Xms1g 3 | -Xmx3g 4 | -Xss2m 5 | -XX:+CMSClassUnloadingEnabled 6 | -XX:+UseConcMarkSweepGC 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/wiki/FAQ) before submitting new issues. 2 | -------------------------------------------------------------------------------- /src/main/assets/fonts/Iceland.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/assets/fonts/Iceland.ttf -------------------------------------------------------------------------------- /local.properties.example: -------------------------------------------------------------------------------- 1 | key.alias: your_key_alias 2 | key.store: /path/to/your/key/store 3 | key.store.password: your_key_password 4 | -------------------------------------------------------------------------------- /src/main/res/raw/gtm_default_container: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/raw/gtm_default_container -------------------------------------------------------------------------------- /src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/drawable/background_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable/background_header.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/values-v21/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -28dp 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-hdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/ic_stat_shadowsocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-hdpi/ic_stat_shadowsocks.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-mdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/ic_stat_shadowsocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-mdpi/ic_stat_shadowsocks.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-xhdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-xhdpi/ic_stat_shadowsocks.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-xxhdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-xxhdpi/ic_stat_shadowsocks.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxxhdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/HEAD/src/main/res/drawable-xxxhdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | APP_ABI := armeabi-v7a x86 2 | APP_PLATFORM := android-16 3 | APP_STL := stlport_static 4 | NDK_TOOLCHAIN_VERSION := clang 5 | -------------------------------------------------------------------------------- /src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 20 | 21 | 29 | 30 | 31 | 35 | 36 | 42 | 45 | 49 | 52 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import android.os.Bundle 24 | import android.support.design.widget.Snackbar 25 | import android.support.v14.preference.SwitchPreference 26 | import be.mygod.preference.PreferenceFragment 27 | import com.github.shadowsocks.utils.{Key, TcpFastOpen} 28 | 29 | class GlobalConfigFragment extends PreferenceFragment { 30 | override def onCreatePreferences(bundle: Bundle, key: String) { 31 | addPreferencesFromResource(R.xml.pref_global) 32 | val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference] 33 | switch.setOnPreferenceChangeListener((_, value) => { 34 | BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean]) 35 | true 36 | }) 37 | if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) { 38 | BootReceiver.setEnabled(getActivity, true) 39 | getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply() 40 | } 41 | switch.setChecked(BootReceiver.getEnabled(getActivity)) 42 | 43 | val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference] 44 | tfo.setChecked(TcpFastOpen.sendEnabled) 45 | tfo.setOnPreferenceChangeListener((_, v) => { 46 | val value = v.asInstanceOf[Boolean] 47 | val result = TcpFastOpen.enabled(value) 48 | if (result != null && result != "Success.") 49 | Snackbar.make(getActivity.findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show() 50 | value == TcpFastOpen.sendEnabled 51 | }) 52 | if (!TcpFastOpen.supported) { 53 | tfo.setEnabled(false) 54 | tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version"))) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks.widget 22 | 23 | import android.support.design.widget.Snackbar 24 | import android.view.View 25 | import com.github.shadowsocks.R 26 | 27 | import scala.collection.mutable.ArrayBuffer 28 | 29 | /** 30 | * @author Mygod 31 | * @param view The view to find a parent from. 32 | * @param undo Callback for undoing removals. 33 | * @param commit Callback for committing removals. 34 | * @tparam T Item type. 35 | */ 36 | class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit, 37 | commit: Iterator[(Int, T)] => Unit = null) { 38 | private val recycleBin = new ArrayBuffer[(Int, T)] 39 | private val removedCallback = new Snackbar.Callback { 40 | override def onDismissed(snackbar: Snackbar, event: Int) { 41 | event match { 42 | case Snackbar.Callback.DISMISS_EVENT_SWIPE | Snackbar.Callback.DISMISS_EVENT_MANUAL | 43 | Snackbar.Callback.DISMISS_EVENT_TIMEOUT => 44 | if (commit != null) commit(recycleBin.iterator) 45 | recycleBin.clear() 46 | case _ => 47 | } 48 | last = null 49 | } 50 | } 51 | private var last: Snackbar = _ 52 | 53 | def remove(items: (Int, T)*) { 54 | recycleBin.appendAll(items) 55 | val count = recycleBin.length 56 | last = Snackbar 57 | .make(view, view.getResources.getQuantityString(R.plurals.removed, count, count: Integer), Snackbar.LENGTH_LONG) 58 | .addCallback(removedCallback) 59 | .setAction(R.string.undo, (_ => { 60 | undo(recycleBin.reverseIterator) 61 | recycleBin.clear 62 | }): View.OnClickListener) 63 | last.show() 64 | } 65 | 66 | def flush(): Unit = if (last != null) last.dismiss() 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/QRCodeDialog.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import java.nio.charset.Charset 24 | 25 | import android.app.Activity 26 | import android.nfc.{NdefMessage, NdefRecord, NfcAdapter} 27 | import android.os.Bundle 28 | import android.view.{LayoutInflater, View, ViewGroup} 29 | import android.widget.{ImageView, LinearLayout} 30 | import com.github.shadowsocks.utils.Utils 31 | import net.glxn.qrgen.android.QRCode 32 | 33 | object QRCodeDialog { 34 | private final val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL" 35 | } 36 | 37 | final class QRCodeDialog extends DialogFragment { 38 | import QRCodeDialog._ 39 | def this(url: String) { 40 | this() 41 | val bundle = new Bundle() 42 | bundle.putString(KEY_URL, url) 43 | setArguments(bundle) 44 | } 45 | private def url = getArguments.getString(KEY_URL) 46 | 47 | private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8")) 48 | private var adapter: NfcAdapter = _ 49 | 50 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = { 51 | val image = new ImageView(getActivity) 52 | image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)) 53 | val qrcode = QRCode.from(url) 54 | .withSize(Utils.dpToPx(getActivity, 250), Utils.dpToPx(getActivity, 250)) 55 | .asInstanceOf[QRCode].bitmap() 56 | image.setImageBitmap(qrcode) 57 | image 58 | } 59 | 60 | override def onAttach(activity: Activity) { 61 | superOnAttach(activity) 62 | adapter = NfcAdapter.getDefaultAdapter(getActivity) 63 | if (adapter != null) adapter.setNdefPushMessage(new NdefMessage(Array( 64 | new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem))), activity) 65 | } 66 | 67 | override def onDetach() { 68 | if (adapter != null) { 69 | adapter.setNdefPushMessage(null, getActivity) 70 | adapter = null 71 | } 72 | super.onDetach() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/NumberPickerPreference.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package be.mygod.preference 22 | 23 | import android.content.Context 24 | import android.content.res.TypedArray 25 | import android.support.v7.preference.DialogPreference 26 | import android.util.AttributeSet 27 | import android.view.ContextThemeWrapper 28 | import android.widget.NumberPicker 29 | import com.github.shadowsocks.R 30 | 31 | class NumberPickerPreference(private val context: Context, attrs: AttributeSet = null) 32 | extends DialogPreference(context, attrs) with DialogPreferencePlus with SummaryPreference { 33 | private[preference] val picker = new NumberPicker(new ContextThemeWrapper(context, R.style.NumberPickerStyle)) 34 | private var value: Int = _ 35 | 36 | { 37 | val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.NumberPickerPreference) 38 | setMin(a.getInt(R.styleable.NumberPickerPreference_min, 0)) 39 | setMax(a.getInt(R.styleable.NumberPickerPreference_max, Int.MaxValue - 1)) 40 | a.recycle() 41 | } 42 | 43 | override def createDialog() = new NumberPickerPreferenceDialogFragment() 44 | 45 | def getValue: Int = value 46 | def getMin: Int = if (picker == null) 0 else picker.getMinValue 47 | def getMax: Int = picker.getMaxValue 48 | def setValue(i: Int) { 49 | if (i == value) return 50 | picker.setValue(i) 51 | value = picker.getValue 52 | persistInt(value) 53 | notifyChanged() 54 | } 55 | def setMin(value: Int): Unit = picker.setMinValue(value) 56 | def setMax(value: Int): Unit = picker.setMaxValue(value) 57 | 58 | override protected def onGetDefaultValue(a: TypedArray, index: Int): AnyRef = 59 | a.getInt(index, getMin).asInstanceOf[AnyRef] 60 | override protected def onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any) { 61 | val default = defaultValue.asInstanceOf[Int] 62 | setValue(if (restorePersistedValue) getPersistedInt(default) else default) 63 | } 64 | protected def getSummaryValue: AnyRef = getValue.asInstanceOf[AnyRef] 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/TrafficMonitor.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks.utils 22 | 23 | import java.text.DecimalFormat 24 | 25 | import com.github.shadowsocks.R 26 | import com.github.shadowsocks.ShadowsocksApplication.app 27 | 28 | object TrafficMonitor { 29 | // Bytes per second 30 | var txRate: Long = _ 31 | var rxRate: Long = _ 32 | 33 | // Bytes for the current session 34 | var txTotal: Long = _ 35 | var rxTotal: Long = _ 36 | 37 | // Bytes for the last query 38 | var txLast: Long = _ 39 | var rxLast: Long = _ 40 | var timestampLast: Long = _ 41 | @volatile var dirty = true 42 | 43 | private val units = Array("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB") 44 | private val numberFormat = new DecimalFormat("@@@") 45 | def formatTraffic(size: Long): String = { 46 | var n: Double = size 47 | var i = -1 48 | while (n >= 1000) { 49 | n /= 1024 50 | i = i + 1 51 | } 52 | if (i < 0) size + " " + app.getResources.getQuantityString(R.plurals.bytes, size.toInt) 53 | else numberFormat.format(n) + ' ' + units(i) 54 | } 55 | 56 | def updateRate(): Boolean = { 57 | val now = System.currentTimeMillis() 58 | val delta = now - timestampLast 59 | var updated = false 60 | if (delta != 0) { 61 | if (dirty) { 62 | txRate = (txTotal - txLast) * 1000 / delta 63 | rxRate = (rxTotal - rxLast) * 1000 / delta 64 | txLast = txTotal 65 | rxLast = rxTotal 66 | dirty = false 67 | updated = true 68 | } else { 69 | if (txRate != 0) { 70 | txRate = 0 71 | updated = true 72 | } 73 | if (rxRate != 0) { 74 | rxRate = 0 75 | updated = true 76 | } 77 | } 78 | timestampLast = now 79 | } 80 | updated 81 | } 82 | 83 | def update(tx: Long, rx: Long) { 84 | if (txTotal != tx) { 85 | txTotal = tx 86 | dirty = true 87 | } 88 | if (rxTotal != rx) { 89 | rxTotal = rx 90 | dirty = true 91 | } 92 | } 93 | 94 | def reset() { 95 | txRate = 0 96 | rxRate = 0 97 | txTotal = 0 98 | rxTotal = 0 99 | txLast = 0 100 | rxLast = 0 101 | dirty = true 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Shadowsocks R for Android 2 | 3 | A [Shadowsocks R](https://github.com/breakwa11/shadowsocks-rss/) client for Android, written in Scala. 4 | 5 | ### CI STATUS 6 | 7 | [![Build Status](https://api.travis-ci.org/shadowsocks/shadowsocks-android.svg)](https://travis-ci.org/shadowsocks/shadowsocks-android) 8 | 9 | ### PREREQUISITES 10 | 11 | * JDK 1.8 12 | * SBT 0.13.0+ 13 | * Go 1.4+ 14 | * Android SDK 15 | - Build Tools 25+ 16 | - Android Support Repository and Google Repository (see `build.sbt` for version) 17 | * Android NDK r12b+ 18 | 19 | ### BUILD 20 | 21 | * Set environment variable `ANDROID_HOME` to `/path/to/android-sdk` 22 | * Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` 23 | * Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go` 24 | * Create your key following the instructions at https://developer.android.com/studio/publish/app-signing.html 25 | * Create `local.properties` from `local.properties.example` with your own key information 26 | * Invoke the building like this 27 | 28 | ```bash 29 | git submodule update --init 30 | 31 | # Build the App 32 | sbt native-build clean android:package-release 33 | ``` 34 | 35 | ### TRANSLATE 36 | 37 | Translators can go to [POEditor](https://poeditor.com/join/project/u5VHO9vhSf) to help translate shadowsocks-android. Guidelines: 38 | 39 | * It's okay to leave some strings untranslated if you think it should use the same string as English (US). 40 | * `faq_url` should not be changed. If you'd like to translate FAQ, submit a pull request with the translated [`faq.md`](https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md) (it should be named properly, e.g. `.github/faq.zh-CN.md`). Administrators will take care of the rest. 41 | * Do not add/edit/remove comments. 42 | 43 | ## OPEN SOURCE LICENSES 44 | 45 |
    46 |
  • redsocks: APL 2.0
  • 47 |
  • mbed TLS: APL 2.0
  • 48 |
  • libevent: BSD
  • 49 |
  • tun2socks: BSD
  • 50 |
  • pcre: BSD
  • 51 |
  • libancillary: BSD
  • 52 |
  • shadowsocksr-libev: GPLv3
  • 53 |
  • shadowsocksr-obfsplugin: MIT
  • 54 |
  • pdnsd: GPLv3
  • 55 |
  • libev: GPLv2
  • 56 |
  • kcptun: MIT
  • 57 |
  • libsodium: ISC
  • 58 |
59 | 60 | ### LICENSE 61 | 62 | Copyright (C) 2017 by Max Lv <> 63 | Copyright (C) 2017 by Mygod Studio <> 64 | 65 | This program is free software: you can redistribute it and/or modify 66 | it under the terms of the GNU General Public License as published by 67 | the Free Software Foundation, either version 3 of the License, or 68 | (at your option) any later version. 69 | 70 | This program is distributed in the hope that it will be useful, 71 | but WITHOUT ANY WARRANTY; without even the implied warranty of 72 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 73 | GNU General Public License for more details. 74 | 75 | You should have received a copy of the GNU General Public License 76 | along with this program. If not, see . 77 | -------------------------------------------------------------------------------- /src/main/jni/include/libev/config.h: -------------------------------------------------------------------------------- 1 | /* config.h. Generated from config.h.in by configure. */ 2 | /* config.h.in. Generated from configure.ac by autoheader. */ 3 | 4 | /* Define to 1 if you have the `clock_gettime' function. */ 5 | /* #undef HAVE_CLOCK_GETTIME */ 6 | 7 | /* Define to 1 to use the syscall interface for clock_gettime */ 8 | #define HAVE_CLOCK_SYSCALL 1 9 | 10 | /* Define to 1 if you have the header file. */ 11 | #define HAVE_DLFCN_H 1 12 | 13 | /* Define to 1 if you have the `epoll_ctl' function. */ 14 | #define HAVE_EPOLL_CTL 1 15 | 16 | /* Define to 1 if you have the `eventfd' function. */ 17 | #define HAVE_EVENTFD 1 18 | 19 | /* Define to 1 if the floor function is available */ 20 | #define HAVE_FLOOR 1 21 | 22 | /* Define to 1 if you have the `inotify_init' function. */ 23 | #define HAVE_INOTIFY_INIT 1 24 | 25 | /* Define to 1 if you have the header file. */ 26 | #define HAVE_INTTYPES_H 1 27 | 28 | /* Define to 1 if you have the `kqueue' function. */ 29 | /* #undef HAVE_KQUEUE */ 30 | 31 | /* Define to 1 if you have the `rt' library (-lrt). */ 32 | /* #undef HAVE_LIBRT */ 33 | 34 | /* Define to 1 if you have the header file. */ 35 | #define HAVE_MEMORY_H 1 36 | 37 | /* Define to 1 if you have the `nanosleep' function. */ 38 | #define HAVE_NANOSLEEP 1 39 | 40 | /* Define to 1 if you have the `poll' function. */ 41 | #define HAVE_POLL 1 42 | 43 | /* Define to 1 if you have the header file. */ 44 | #define HAVE_POLL_H 1 45 | 46 | /* Define to 1 if you have the `port_create' function. */ 47 | /* #undef HAVE_PORT_CREATE */ 48 | 49 | /* Define to 1 if you have the header file. */ 50 | /* #undef HAVE_PORT_H */ 51 | 52 | /* Define to 1 if you have the `select' function. */ 53 | #define HAVE_SELECT 1 54 | 55 | /* Define to 1 if you have the `signalfd' function. */ 56 | #define HAVE_SIGNALFD 0 57 | 58 | /* Define to 1 if you have the header file. */ 59 | #define HAVE_STDINT_H 1 60 | 61 | /* Define to 1 if you have the header file. */ 62 | #define HAVE_STDLIB_H 1 63 | 64 | /* Define to 1 if you have the header file. */ 65 | #define HAVE_STRINGS_H 1 66 | 67 | /* Define to 1 if you have the header file. */ 68 | #define HAVE_STRING_H 1 69 | 70 | /* Define to 1 if you have the header file. */ 71 | #define HAVE_SYS_EPOLL_H 1 72 | 73 | /* Define to 1 if you have the header file. */ 74 | #define HAVE_SYS_EVENTFD_H 1 75 | 76 | /* Define to 1 if you have the header file. */ 77 | /* #undef HAVE_SYS_EVENT_H */ 78 | 79 | /* Define to 1 if you have the header file. */ 80 | #define HAVE_SYS_INOTIFY_H 1 81 | 82 | /* Define to 1 if you have the header file. */ 83 | #define HAVE_SYS_SELECT_H 1 84 | 85 | /* Define to 1 if you have the header file. */ 86 | #define HAVE_SYS_SIGNALFD_H 1 87 | 88 | /* Define to 1 if you have the header file. */ 89 | #define HAVE_SYS_STAT_H 1 90 | 91 | /* Define to 1 if you have the header file. */ 92 | #define HAVE_SYS_TYPES_H 1 93 | 94 | /* Define to 1 if you have the header file. */ 95 | #define HAVE_UNISTD_H 1 96 | 97 | /* Define to the sub-directory in which libtool stores uninstalled libraries. 98 | */ 99 | #define LT_OBJDIR ".libs/" 100 | 101 | /* Name of package */ 102 | #define PACKAGE "libev" 103 | 104 | /* Define to the address where bug reports for this package should be sent. */ 105 | #define PACKAGE_BUGREPORT "" 106 | 107 | /* Define to the full name of this package. */ 108 | #define PACKAGE_NAME "" 109 | 110 | /* Define to the full name and version of this package. */ 111 | #define PACKAGE_STRING "" 112 | 113 | /* Define to the one symbol short name of this package. */ 114 | #define PACKAGE_TARNAME "" 115 | 116 | /* Define to the home page for this package. */ 117 | #define PACKAGE_URL "" 118 | 119 | /* Define to the version of this package. */ 120 | #define PACKAGE_VERSION "" 121 | 122 | /* Define to 1 if you have the ANSI C header files. */ 123 | #define STDC_HEADERS 1 124 | 125 | /* Version number of package */ 126 | #define VERSION "4.11" 127 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/ShadowsocksTileService.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import android.annotation.TargetApi 24 | import android.graphics.drawable.Icon 25 | import android.service.quicksettings.{Tile, TileService} 26 | import com.github.shadowsocks.aidl.IShadowsocksServiceCallback 27 | import com.github.shadowsocks.utils.{State, Utils} 28 | 29 | /** 30 | * @author Mygod 31 | */ 32 | @TargetApi(24) 33 | final class ShadowsocksTileService extends TileService with ServiceBoundContext { 34 | 35 | private lazy val iconIdle = Icon.createWithResource(this, R.drawable.ic_start_idle).setTint(0x80ffffff) 36 | private lazy val iconBusy = Icon.createWithResource(this, R.drawable.ic_start_busy) 37 | private lazy val iconConnected = Icon.createWithResource(this, R.drawable.ic_start_connected) 38 | private lazy val callback = new IShadowsocksServiceCallback.Stub { 39 | def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = () 40 | def stateChanged(state: Int, profileName: String, msg: String) { 41 | val tile = getQsTile 42 | if (tile != null) { 43 | state match { 44 | case State.STOPPED => 45 | tile.setIcon(iconIdle) 46 | tile.setLabel(getString(R.string.app_name)) 47 | tile.setState(Tile.STATE_INACTIVE) 48 | case State.CONNECTED => 49 | tile.setIcon(iconConnected) 50 | tile.setLabel(if (profileName == null) getString(R.string.app_name) else profileName) 51 | tile.setState(Tile.STATE_ACTIVE) 52 | case _ => 53 | tile.setIcon(iconBusy) 54 | tile.setLabel(getString(R.string.app_name)) 55 | tile.setState(Tile.STATE_UNAVAILABLE) 56 | } 57 | tile.updateTile() 58 | } 59 | } 60 | override def trafficPersisted(profileId: Int): Unit = () 61 | } 62 | 63 | override def onServiceConnected(): Unit = callback.stateChanged(bgService.getState, bgService.getProfileName, null) 64 | 65 | override def onStartListening() { 66 | super.onStartListening() 67 | attachService(callback) 68 | } 69 | override def onStopListening() { 70 | super.onStopListening() 71 | detachService() // just in case the user switches to NAT mode, also saves battery 72 | } 73 | 74 | override def onClick(): Unit = if (isLocked) unlockAndRun(toggle) else toggle() 75 | 76 | private def toggle() = if (bgService != null) bgService.getState match { 77 | case State.STOPPED => Utils.startSsService(this) 78 | case State.CONNECTED => Utils.stopSsService(this) 79 | case _ => // ignore 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import android.app.{Activity, KeyguardManager} 24 | import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} 25 | import android.net.VpnService 26 | import android.os.{Bundle, Handler} 27 | import android.util.Log 28 | import com.github.shadowsocks.ShadowsocksApplication.app 29 | 30 | object ShadowsocksRunnerActivity { 31 | private final val TAG = "ShadowsocksRunnerActivity" 32 | private final val REQUEST_CONNECT = 1 33 | } 34 | 35 | class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext { 36 | import ShadowsocksRunnerActivity._ 37 | 38 | val handler = new Handler() 39 | 40 | // Variables 41 | var receiver: BroadcastReceiver = _ 42 | 43 | override def onServiceConnected() { 44 | handler.postDelayed(() => if (bgService != null) startBackgroundService(), 1000) 45 | } 46 | 47 | def startBackgroundService() { 48 | if (app.isNatEnabled) { 49 | bgService.use(app.profileId) 50 | finish() 51 | } else { 52 | val intent = VpnService.prepare(ShadowsocksRunnerActivity.this) 53 | if (intent != null) { 54 | startActivityForResult(intent, REQUEST_CONNECT) 55 | } else { 56 | onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null) 57 | } 58 | } 59 | } 60 | 61 | override def onCreate(savedInstanceState: Bundle) { 62 | super.onCreate(savedInstanceState) 63 | val km = getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager] 64 | val locked = km.inKeyguardRestrictedInputMode 65 | if (locked) { 66 | val filter = new IntentFilter(Intent.ACTION_USER_PRESENT) 67 | receiver = (_: Context, intent: Intent) => { 68 | if (intent.getAction == Intent.ACTION_USER_PRESENT) { 69 | attachService() 70 | } 71 | } 72 | registerReceiver(receiver, filter) 73 | } else { 74 | attachService() 75 | } 76 | finish() 77 | } 78 | 79 | override def onDestroy() { 80 | super.onDestroy() 81 | detachService() 82 | if (receiver != null) { 83 | unregisterReceiver(receiver) 84 | receiver = null 85 | } 86 | } 87 | 88 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { 89 | resultCode match { 90 | case Activity.RESULT_OK => 91 | if (bgService != null) { 92 | bgService.use(app.profileId) 93 | } 94 | case _ => 95 | Log.e(TAG, "Failed to start VpnService") 96 | } 97 | finish() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/TrafficMonitorThread.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks.utils 22 | 23 | import java.io.{File, IOException} 24 | import java.nio.{ByteBuffer, ByteOrder} 25 | import java.util.concurrent.Executors 26 | 27 | import android.content.Context 28 | import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress} 29 | import android.util.Log 30 | import com.github.shadowsocks.ShadowsocksApplication.app 31 | 32 | class TrafficMonitorThread(context: Context) extends Thread { 33 | 34 | val TAG = "TrafficMonitorThread" 35 | lazy val PATH: String = context.getApplicationInfo.dataDir + "/stat_path" 36 | 37 | @volatile var serverSocket: LocalServerSocket = _ 38 | @volatile var isRunning: Boolean = true 39 | 40 | def closeServerSocket() { 41 | if (serverSocket != null) { 42 | try { 43 | serverSocket.close() 44 | } catch { 45 | case _: Exception => // ignore 46 | } 47 | serverSocket = null 48 | } 49 | } 50 | 51 | def stopThread() { 52 | isRunning = false 53 | closeServerSocket() 54 | } 55 | 56 | override def run() { 57 | 58 | new File(PATH).delete() 59 | 60 | try { 61 | val localSocket = new LocalSocket 62 | localSocket.bind(new LocalSocketAddress(PATH, LocalSocketAddress.Namespace.FILESYSTEM)) 63 | serverSocket = new LocalServerSocket(localSocket.getFileDescriptor) 64 | } catch { 65 | case e: IOException => 66 | Log.e(TAG, "unable to bind", e) 67 | return 68 | } 69 | 70 | val pool = Executors.newFixedThreadPool(1) 71 | 72 | while (isRunning) { 73 | try { 74 | val socket = serverSocket.accept() 75 | 76 | pool.execute(() => { 77 | try { 78 | val input = socket.getInputStream 79 | val output = socket.getOutputStream 80 | 81 | val buffer = new Array[Byte](16) 82 | if (input.read(buffer) != 16) throw new IOException("Unexpected traffic stat length") 83 | val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN) 84 | TrafficMonitor.update(stat.getLong(0), stat.getLong(8)) 85 | 86 | output.write(0) 87 | 88 | input.close() 89 | output.close() 90 | 91 | } catch { 92 | case e: Exception => 93 | Log.e(TAG, "Error when recv traffic stat", e) 94 | app.track(e) 95 | } 96 | 97 | // close socket 98 | try { 99 | socket.close() 100 | } catch { 101 | case _: Exception => // ignore 102 | } 103 | 104 | }) 105 | } catch { 106 | case e: IOException => 107 | Log.e(TAG, "Error when accept socket", e) 108 | app.track(e) 109 | return 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/jni/system.cpp: -------------------------------------------------------------------------------- 1 | #define LOG_TAG "Shadowsocks" 2 | 3 | #include "jni.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0) 17 | #define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0) 18 | #define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0) 19 | 20 | jint Java_com_github_shadowsocks_system_exec(JNIEnv *env, jobject thiz, jstring cmd) { 21 | const char *cmd_str = env->GetStringUTFChars(cmd, 0); 22 | 23 | pid_t pid; 24 | 25 | /* Fork off the parent process */ 26 | pid = fork(); 27 | if (pid < 0) { 28 | env->ReleaseStringUTFChars(cmd, cmd_str); 29 | return -1; 30 | } 31 | 32 | if (pid > 0) { 33 | env->ReleaseStringUTFChars(cmd, cmd_str); 34 | return pid; 35 | } 36 | 37 | execl("/system/bin/sh", "sh", "-c", cmd_str, NULL); 38 | env->ReleaseStringUTFChars(cmd, cmd_str); 39 | 40 | return 1; 41 | } 42 | 43 | void Java_com_github_shadowsocks_system_jniclose(JNIEnv *env, jobject thiz, jint fd) { 44 | close(fd); 45 | } 46 | 47 | jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) { 48 | int fd; 49 | struct sockaddr_un addr; 50 | const char *sock_str = env->GetStringUTFChars(path, 0); 51 | 52 | if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 53 | LOGE("socket() failed: %s (socket fd = %d)\n", strerror(errno), fd); 54 | return (jint)-1; 55 | } 56 | 57 | memset(&addr, 0, sizeof(addr)); 58 | addr.sun_family = AF_UNIX; 59 | strncpy(addr.sun_path, sock_str, sizeof(addr.sun_path)-1); 60 | 61 | if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { 62 | LOGE("connect() failed: %s (fd = %d)\n", strerror(errno), fd); 63 | close(fd); 64 | return (jint)-1; 65 | } 66 | 67 | if (ancil_send_fd(fd, tun_fd)) { 68 | LOGE("ancil_send_fd: %s", strerror(errno)); 69 | close(fd); 70 | return (jint)-1; 71 | } 72 | 73 | close(fd); 74 | env->ReleaseStringUTFChars(path, sock_str); 75 | return 0; 76 | } 77 | 78 | static const char *classPathName = "com/github/shadowsocks/System"; 79 | 80 | static JNINativeMethod method_table[] = { 81 | { "jniclose", "(I)V", 82 | (void*) Java_com_github_shadowsocks_system_jniclose }, 83 | { "sendfd", "(ILjava/lang/String;)I", 84 | (void*) Java_com_github_shadowsocks_system_sendfd }, 85 | { "exec", "(Ljava/lang/String;)I", 86 | (void*) Java_com_github_shadowsocks_system_exec } 87 | }; 88 | 89 | 90 | 91 | /* 92 | * Register several native methods for one class. 93 | */ 94 | static int registerNativeMethods(JNIEnv* env, const char* className, 95 | JNINativeMethod* gMethods, int numMethods) 96 | { 97 | jclass clazz; 98 | 99 | clazz = env->FindClass(className); 100 | if (clazz == NULL) { 101 | LOGE("Native registration unable to find class '%s'", className); 102 | return JNI_FALSE; 103 | } 104 | if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { 105 | LOGE("RegisterNatives failed for '%s'", className); 106 | return JNI_FALSE; 107 | } 108 | 109 | return JNI_TRUE; 110 | } 111 | 112 | /* 113 | * Register native methods for all classes we know about. 114 | * 115 | * returns JNI_TRUE on success. 116 | */ 117 | static int registerNatives(JNIEnv* env) 118 | { 119 | if (!registerNativeMethods(env, classPathName, method_table, 120 | sizeof(method_table) / sizeof(method_table[0]))) { 121 | return JNI_FALSE; 122 | } 123 | 124 | return JNI_TRUE; 125 | } 126 | 127 | /* 128 | * This is called by the VM when the shared library is first loaded. 129 | */ 130 | 131 | typedef union { 132 | JNIEnv* env; 133 | void* venv; 134 | } UnionJNIEnvToVoid; 135 | 136 | jint JNI_OnLoad(JavaVM* vm, void* reserved) { 137 | UnionJNIEnvToVoid uenv; 138 | uenv.venv = NULL; 139 | jint result = -1; 140 | JNIEnv* env = NULL; 141 | 142 | LOGI("JNI_OnLoad"); 143 | 144 | if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) { 145 | LOGE("ERROR: GetEnv failed"); 146 | goto bail; 147 | } 148 | env = uenv.env; 149 | 150 | if (registerNatives(env) != JNI_TRUE) { 151 | LOGE("ERROR: registerNatives failed"); 152 | goto bail; 153 | } 154 | 155 | result = JNI_VERSION_1_4; 156 | 157 | bail: 158 | return result; 159 | } 160 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/GuardedProcess.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import java.io._ 24 | import java.lang.System.currentTimeMillis 25 | import java.util.concurrent.Semaphore 26 | 27 | import android.util.Log 28 | import com.github.shadowsocks.utils.CloseUtils._ 29 | 30 | import scala.collection.JavaConversions._ 31 | import scala.collection.immutable.Stream 32 | 33 | class StreamLogger(is: InputStream, tag: String) extends Thread { 34 | override def run(): Unit = autoClose(new BufferedReader(new InputStreamReader(is)))(br => 35 | try Stream.continually(br.readLine()).takeWhile(_ != null).foreach(Log.i(tag, _)) catch { 36 | case _: IOException => 37 | }) 38 | } 39 | 40 | /** 41 | * @author ayanamist@gmail.com 42 | */ 43 | class GuardedProcess(cmd: Seq[String]) { 44 | private val TAG = classOf[GuardedProcess].getSimpleName 45 | 46 | @volatile private var guardThread: Thread = _ 47 | @volatile private var isDestroyed: Boolean = _ 48 | @volatile private var process: Process = _ 49 | @volatile private var isRestart = false 50 | 51 | def start(onRestartCallback: () => Unit = null): GuardedProcess = { 52 | val semaphore = new Semaphore(1) 53 | semaphore.acquire() 54 | @volatile var ioException: IOException = null 55 | 56 | guardThread = new Thread(() => { 57 | try { 58 | var callback: () => Unit = null 59 | while (!isDestroyed) { 60 | Log.i(TAG, "start process: " + cmd) 61 | val startTime = currentTimeMillis 62 | 63 | process = new ProcessBuilder(cmd).redirectErrorStream(true).start 64 | 65 | val is = process.getInputStream 66 | new StreamLogger(is, TAG).start() 67 | 68 | if (callback == null) callback = onRestartCallback else callback() 69 | 70 | semaphore.release() 71 | process.waitFor 72 | 73 | this.synchronized { 74 | if (isRestart) { 75 | isRestart = false 76 | } else { 77 | if (currentTimeMillis - startTime < 1000) { 78 | Log.w(TAG, "process exit too fast, stop guard: " + cmd) 79 | isDestroyed = true 80 | } 81 | } 82 | } 83 | } 84 | } catch { 85 | case _: InterruptedException => 86 | Log.i(TAG, "thread interrupt, destroy process: " + cmd) 87 | process.destroy() 88 | case e: IOException => ioException = e 89 | } finally semaphore.release() 90 | }, "GuardThread-" + cmd) 91 | 92 | guardThread.start() 93 | semaphore.acquire() 94 | 95 | if (ioException != null) throw ioException 96 | 97 | this 98 | } 99 | 100 | def destroy() { 101 | isDestroyed = true 102 | guardThread.interrupt() 103 | process.destroy() 104 | try guardThread.join() catch { 105 | case _: InterruptedException => 106 | } 107 | } 108 | 109 | def restart() { 110 | this.synchronized { 111 | isRestart = true 112 | process.destroy() 113 | } 114 | } 115 | 116 | @throws(classOf[InterruptedException]) 117 | def waitFor: Int = { 118 | guardThread.join() 119 | 0 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package be.mygod.preference 22 | 23 | import java.lang.reflect.Field 24 | import java.util 25 | 26 | import android.os.Build 27 | import android.support.v4.content.ContextCompat 28 | import android.support.v4.view.ViewCompat 29 | import android.support.v7.preference.{PreferenceGroup, PreferenceViewHolder, PreferenceGroupAdapter => Old} 30 | import android.view.{LayoutInflater, View, ViewGroup} 31 | import com.github.shadowsocks.R 32 | 33 | /** 34 | * Fix by: https://github.com/Gericop/Android-Support-Preference-V7-Fix/commit/7de016b007e28a264001a8bb353f110a7f64bb69 35 | * 36 | * @author Mygod 37 | */ 38 | object PreferenceGroupAdapter { 39 | private var preferenceLayoutsField: Field = _ 40 | private var fieldResId: Field = _ 41 | private var fieldWidgetResId: Field = _ 42 | private val preferenceViewHolderConstructor = classOf[PreferenceViewHolder].getDeclaredConstructor(classOf[View]) 43 | 44 | { 45 | val oldClass = classOf[Old] 46 | preferenceLayoutsField = oldClass.getDeclaredField("mPreferenceLayouts") 47 | preferenceLayoutsField.setAccessible(true) 48 | val c = oldClass.getDeclaredClasses.filter(c => c.getSimpleName == "PreferenceLayout").head 49 | fieldResId = c.getDeclaredField("resId") 50 | fieldResId.setAccessible(true) 51 | fieldWidgetResId = c.getDeclaredField("widgetResId") 52 | fieldWidgetResId.setAccessible(true) 53 | preferenceViewHolderConstructor.setAccessible(true) 54 | } 55 | } 56 | 57 | class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) { 58 | import PreferenceGroupAdapter._ 59 | 60 | protected lazy val preferenceLayouts: util.List[AnyRef] = 61 | preferenceLayoutsField.get(this).asInstanceOf[util.List[AnyRef]] 62 | 63 | override def onCreateViewHolder(parent: ViewGroup, viewType: Int): PreferenceViewHolder = 64 | if (Build.VERSION.SDK_INT < 21) { 65 | val context = parent.getContext 66 | val inflater = LayoutInflater.from(context) 67 | val pl = preferenceLayouts.get(viewType) 68 | val view = inflater.inflate(fieldResId.get(pl).asInstanceOf[Int], parent, false) 69 | if (view.getBackground == null) { 70 | val array = context.obtainStyledAttributes(null, R.styleable.BackgroundStyle) 71 | var background = array.getDrawable(R.styleable.BackgroundStyle_android_selectableItemBackground) 72 | if (background == null) 73 | background = ContextCompat.getDrawable(context, android.R.drawable.list_selector_background) 74 | array.recycle() 75 | val (s, t, e, b) = (ViewCompat.getPaddingStart(view), view.getPaddingTop, 76 | ViewCompat.getPaddingEnd(view), view.getPaddingBottom) 77 | view.setBackground(background) 78 | ViewCompat.setPaddingRelative(view, s, t, e, b) 79 | } 80 | val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup] 81 | if (widgetFrame != null) { 82 | val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int] 83 | if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE) 84 | } 85 | preferenceViewHolderConstructor.newInstance(view) 86 | } else super.onCreateViewHolder(parent, viewType) 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/ShadowsocksVpnThread.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks 22 | 23 | import java.io.{File, FileDescriptor, IOException} 24 | import java.lang.reflect.Method 25 | import java.util.concurrent.Executors 26 | 27 | import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress} 28 | import android.util.Log 29 | import com.github.shadowsocks.ShadowsocksApplication.app 30 | 31 | object ShadowsocksVpnThread { 32 | val getInt: Method = classOf[FileDescriptor].getDeclaredMethod("getInt$") 33 | } 34 | 35 | class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread { 36 | import ShadowsocksVpnThread._ 37 | 38 | val TAG = "ShadowsocksVpnService" 39 | lazy val PATH: String = vpnService.getApplicationInfo.dataDir + "/protect_path" 40 | 41 | @volatile var isRunning: Boolean = true 42 | @volatile var serverSocket: LocalServerSocket = _ 43 | 44 | def closeServerSocket() { 45 | if (serverSocket != null) { 46 | try { 47 | serverSocket.close() 48 | } catch { 49 | case _: Exception => // ignore 50 | } 51 | serverSocket = null 52 | } 53 | } 54 | 55 | def stopThread() { 56 | isRunning = false 57 | closeServerSocket() 58 | } 59 | 60 | override def run() { 61 | 62 | new File(PATH).delete() 63 | 64 | try { 65 | val localSocket = new LocalSocket 66 | localSocket.bind(new LocalSocketAddress(PATH, LocalSocketAddress.Namespace.FILESYSTEM)) 67 | serverSocket = new LocalServerSocket(localSocket.getFileDescriptor) 68 | } catch { 69 | case e: IOException => 70 | Log.e(TAG, "unable to bind", e) 71 | app.track(e) 72 | return 73 | } 74 | 75 | val pool = Executors.newFixedThreadPool(4) 76 | 77 | while (isRunning) { 78 | try { 79 | val socket = serverSocket.accept() 80 | 81 | pool.execute(() => { 82 | try { 83 | val input = socket.getInputStream 84 | val output = socket.getOutputStream 85 | 86 | input.read() 87 | 88 | val fds = socket.getAncillaryFileDescriptors 89 | 90 | if (fds.nonEmpty) { 91 | val fd = getInt.invoke(fds(0)).asInstanceOf[Int] 92 | val ret = vpnService.protect(fd) 93 | 94 | // Trick to close file decriptor 95 | System.jniclose(fd) 96 | 97 | if (ret) { 98 | output.write(0) 99 | } else { 100 | output.write(1) 101 | } 102 | } 103 | 104 | input.close() 105 | output.close() 106 | 107 | } catch { 108 | case e: Exception => 109 | Log.e(TAG, "Error when protect socket", e) 110 | app.track(e) 111 | } 112 | 113 | // close socket 114 | try { 115 | socket.close() 116 | } catch { 117 | case _: Exception => // ignore 118 | } 119 | 120 | }) 121 | } catch { 122 | case e: IOException => 123 | Log.e(TAG, "Error when accept socket", e) 124 | app.track(e) 125 | return 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/database/ProfileManager.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks.database 22 | 23 | import android.util.Log 24 | import com.github.shadowsocks.ProfilesFragment 25 | import com.github.shadowsocks.ShadowsocksApplication.app 26 | 27 | object ProfileManager { 28 | private final val TAG = "ProfileManager" 29 | } 30 | 31 | class ProfileManager(dbHelper: DBHelper) { 32 | import ProfileManager._ 33 | 34 | def createProfile(p: Profile = null): Profile = { 35 | val profile = if (p == null) new Profile else p 36 | profile.id = 0 37 | try { 38 | app.currentProfile match { 39 | case Some(oldProfile) => 40 | // Copy Feature Settings from old profile 41 | profile.route = oldProfile.route 42 | profile.ipv6 = oldProfile.ipv6 43 | profile.proxyApps = oldProfile.proxyApps 44 | profile.bypass = oldProfile.bypass 45 | profile.individual = oldProfile.individual 46 | profile.udpdns = oldProfile.udpdns 47 | case _ => 48 | } 49 | val last = dbHelper.profileDao.queryRaw(dbHelper.profileDao.queryBuilder.selectRaw("MAX(userOrder)") 50 | .prepareStatementString).getFirstResult 51 | if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1 52 | dbHelper.profileDao.createOrUpdate(profile) 53 | if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.add(profile) 54 | } catch { 55 | case ex: Exception => 56 | Log.e(TAG, "addProfile", ex) 57 | app.track(ex) 58 | } 59 | profile 60 | } 61 | 62 | def updateProfile(profile: Profile): Boolean = { 63 | try { 64 | dbHelper.profileDao.update(profile) 65 | true 66 | } catch { 67 | case ex: Exception => 68 | Log.e(TAG, "updateProfile", ex) 69 | app.track(ex) 70 | false 71 | } 72 | } 73 | 74 | def getProfile(id: Int): Option[Profile] = { 75 | try { 76 | dbHelper.profileDao.queryForId(id) match { 77 | case profile: Profile => Option(profile) 78 | case _ => None 79 | } 80 | } catch { 81 | case ex: Exception => 82 | Log.e(TAG, "getProfile", ex) 83 | app.track(ex) 84 | None 85 | } 86 | } 87 | 88 | def delProfile(id: Int): Boolean = { 89 | try { 90 | dbHelper.profileDao.deleteById(id) 91 | if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.removeId(id) 92 | true 93 | } catch { 94 | case ex: Exception => 95 | Log.e(TAG, "delProfile", ex) 96 | app.track(ex) 97 | false 98 | } 99 | } 100 | 101 | def getFirstProfile: Option[Profile] = { 102 | try { 103 | val result = dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.limit(1L).prepare) 104 | if (result.size == 1) Option(result.get(0)) else None 105 | } catch { 106 | case ex: Exception => 107 | Log.e(TAG, "getAllProfiles", ex) 108 | app.track(ex) 109 | None 110 | } 111 | } 112 | 113 | def getAllProfiles: Option[List[Profile]] = { 114 | try { 115 | import scala.collection.JavaConversions._ 116 | Option(dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.orderBy("userOrder", true).prepare).toList) 117 | } catch { 118 | case ex: Exception => 119 | Log.e(TAG, "getAllProfiles", ex) 120 | app.track(ex) 121 | None 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 15 | 23 | 27 | 37 | 47 | 56 | 57 | 62 | 70 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/Parser.scala: -------------------------------------------------------------------------------- 1 | /*******************************************************************************/ 2 | /* */ 3 | /* Copyright (C) 2017 by Max Lv */ 4 | /* Copyright (C) 2017 by Mygod Studio */ 5 | /* */ 6 | /* This program is free software: you can redistribute it and/or modify */ 7 | /* it under the terms of the GNU General Public License as published by */ 8 | /* the Free Software Foundation, either version 3 of the License, or */ 9 | /* (at your option) any later version. */ 10 | /* */ 11 | /* This program is distributed in the hope that it will be useful, */ 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 14 | /* GNU General Public License for more details. */ 15 | /* */ 16 | /* You should have received a copy of the GNU General Public License */ 17 | /* along with this program. If not, see . */ 18 | /* */ 19 | /*******************************************************************************/ 20 | 21 | package com.github.shadowsocks.utils 22 | 23 | import java.net.URLDecoder 24 | 25 | import android.util.{Base64, Log} 26 | import com.github.shadowsocks.database.Profile 27 | 28 | object Parser { 29 | val TAG = "ShadowParser" 30 | private val pattern = "(?i)ss://([A-Za-z0-9+-/=_]+)(#(.+))?".r 31 | private val decodedPattern = "(?i)^((.+?)(-auth)??:(.*)@(.+?):(\\d+?))$".r 32 | 33 | private val pattern_ssr = "(?i)ssr://([A-Za-z0-9_=-]+)".r 34 | private val decodedPattern_ssr = "(?i)^((.+):(\\d+?):(.*):(.+):(.*):([^/]+))".r 35 | private val decodedPattern_ssr_obfsparam = "(?i)[?&]obfsparam=([A-Za-z0-9_=-]*)".r 36 | private val decodedPattern_ssr_remarks = "(?i)[?&]remarks=([A-Za-z0-9_=-]*)".r 37 | private val decodedPattern_ssr_protocolparam = "(?i)[?&]protoparam=([A-Za-z0-9_=-]*)".r 38 | 39 | def findAll(data: CharSequence): Iterator[Profile] = 40 | pattern.findAllMatchIn(if (data == null) "" else data).map(m => try 41 | decodedPattern.findFirstMatchIn(new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")) match { 42 | case Some(ss) => 43 | val profile = new Profile 44 | profile.method = ss.group(2).toLowerCase 45 | if (ss.group(3) != null) profile.protocol = "verify_sha1" 46 | profile.password = ss.group(4) 47 | profile.host = ss.group(5) 48 | profile.remotePort = ss.group(6).toInt 49 | if (m.group(2) != null) profile.name = URLDecoder.decode(m.group(3), "utf-8") 50 | profile 51 | case _ => null 52 | } catch { 53 | case ex: Exception => 54 | Log.e(TAG, "parser error: " + m.source, ex)// Ignore 55 | null 56 | }).filter(_ != null) 57 | 58 | def findAll_ssr(data: CharSequence): Iterator[Profile] = 59 | pattern_ssr.findAllMatchIn(if (data == null) "" else data).map(m => try { 60 | val uri = new String(Base64.decode(m.group(1).replaceAll("=", ""), Base64.NO_PADDING | Base64.URL_SAFE), "UTF-8") 61 | decodedPattern_ssr.findFirstMatchIn(uri) match { 62 | case Some(ss) => 63 | val profile = new Profile 64 | profile.host = ss.group(2).toLowerCase 65 | profile.remotePort = ss.group(3).toInt 66 | profile.protocol = ss.group(4).toLowerCase 67 | profile.method = ss.group(5).toLowerCase 68 | profile.obfs = ss.group(6).toLowerCase 69 | profile.password = new String(Base64.decode(ss.group(7).replaceAll("=", ""), Base64.NO_PADDING | Base64.URL_SAFE), "UTF-8") 70 | 71 | decodedPattern_ssr_obfsparam.findFirstMatchIn(uri) match { 72 | case Some(param) => 73 | profile.obfs_param = new String(Base64.decode(param.group(1).replaceAll("=", ""), Base64.NO_PADDING | Base64.URL_SAFE), "UTF-8") 74 | case _ => null 75 | } 76 | 77 | decodedPattern_ssr_protocolparam.findFirstMatchIn(uri) match { 78 | case Some(param) => 79 | profile.protocol_param = new String(Base64.decode(param.group(1).replaceAll("=", ""), Base64.NO_PADDING | Base64.URL_SAFE), "UTF-8") 80 | case _ => null 81 | } 82 | 83 | decodedPattern_ssr_remarks.findFirstMatchIn(uri) match { 84 | case Some(param) => 85 | profile.name = new String(Base64.decode(param.group(1).replaceAll("=", ""), Base64.NO_PADDING | Base64.URL_SAFE), "UTF-8") 86 | case _ => profile.name = ss.group(2).toLowerCase 87 | } 88 | 89 | profile 90 | case _ => null 91 | } 92 | } catch { 93 | case ex: Exception => 94 | Log.e(TAG, "parser error: " + m.source, ex)// Ignore 95 | null 96 | }).filter(_ != null) 97 | } 98 | --------------------------------------------------------------------------------