├── .github ├── faq.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .gitmodules ├── .headache.cfg ├── .jvmopts ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sbt ├── build.sh ├── gfwlist ├── gen.pl └── gen.py ├── kcptun └── make.bash ├── lint.xml ├── local.properties.example ├── project └── plugins.sbt ├── shadow-notify.png ├── shadow.png └── src └── main ├── AndroidManifest.xml ├── aidl └── com │ └── github │ └── shadowsocks │ └── aidl │ ├── IShadowsocksService.aidl │ └── IShadowsocksServiceCallback.aidl ├── assets ├── acl │ ├── bypass-china.acl │ ├── bypass-lan-china.acl │ ├── bypass-lan.acl │ ├── china-list.acl │ └── gfwlist.acl ├── fonts │ └── Iceland.ttf └── pages │ └── about.html ├── ic_launcher-web.png ├── java └── com │ └── github │ └── shadowsocks │ ├── DialogFragment.java │ └── System.java ├── jni ├── Android.mk ├── Application.mk ├── build-shared-executable.mk ├── include │ ├── libev │ │ └── config.h │ ├── pdnsd │ │ └── config.h │ ├── shadowsocks-libev │ │ └── config.h │ └── sodium │ │ └── version.h └── system.cpp ├── res ├── drawable-anydpi-v21 │ └── ic_navigation_close.xml ├── drawable-hdpi │ ├── ic_navigation_close.png │ └── ic_stat_shadowsocks.png ├── drawable-mdpi │ ├── ic_navigation_close.png │ └── ic_stat_shadowsocks.png ├── drawable-v21 │ ├── background_selectable.xml │ └── background_stat.xml ├── 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 │ ├── background_header.png │ ├── background_profile.xml │ ├── background_selectable.xml │ ├── background_stat.xml │ ├── ic_action_assignment.xml │ ├── ic_action_copyright.xml │ ├── ic_action_delete.xml │ ├── ic_action_description.xml │ ├── ic_action_done.xml │ ├── ic_action_help_outline.xml │ ├── ic_action_note_add.xml │ ├── ic_action_settings.xml │ ├── ic_av_playlist_add.xml │ ├── ic_content_copy.xml │ ├── ic_content_paste.xml │ ├── ic_image_edit.xml │ ├── ic_navigation_refresh.xml │ ├── ic_qu_shadowsocks_launcher.xml │ ├── ic_social_share.xml │ ├── ic_start_busy.xml │ ├── ic_start_connected.xml │ └── ic_start_idle.xml ├── layout │ ├── dialog_acl_rule.xml │ ├── layout_about.xml │ ├── layout_apps.xml │ ├── layout_apps_item.xml │ ├── layout_custom_rules.xml │ ├── layout_global_settings.xml │ ├── layout_header.xml │ ├── layout_list.xml │ ├── layout_main.xml │ ├── layout_profile.xml │ ├── layout_profile_config.xml │ ├── layout_tasker.xml │ └── toolbar_light_dark.xml ├── menu │ ├── app_manager_menu.xml │ ├── custom_rules_menu.xml │ ├── profile_config_menu.xml │ ├── profile_manager_menu.xml │ └── profile_share_popup.xml ├── mipmap-hdpi │ └── ic_launcher.png ├── mipmap-mdpi │ └── ic_launcher.png ├── mipmap-xhdpi │ └── ic_launcher.png ├── mipmap-xxhdpi │ └── ic_launcher.png ├── mipmap-xxxhdpi │ └── ic_launcher.png ├── raw │ └── gtm_default_container ├── values-ja │ └── strings.xml ├── values-ru │ └── strings.xml ├── values-v21 │ ├── dimen.xml │ ├── strings.xml │ └── styles.xml ├── values-zh-rCN │ └── strings.xml ├── values-zh-rTW │ └── strings.xml ├── values │ ├── arrays.xml │ ├── attrs.xml │ ├── colors.xml │ ├── configs.xml │ ├── dimen.xml │ ├── strings.xml │ └── styles.xml └── xml │ ├── pref_global.xml │ ├── pref_profile.xml │ ├── shortcuts.xml │ └── tracker.xml └── scala ├── be └── mygod │ └── preference │ ├── DialogPreferencePlus.scala │ ├── EditTextPreference.scala │ ├── EditTextPreferenceDialogFragment.scala │ ├── NumberPickerPreference.scala │ ├── NumberPickerPreferenceDialogFragment.scala │ ├── PreferenceCategory.scala │ ├── PreferenceFragment.scala │ ├── PreferenceGroupAdapter.scala │ └── SummaryPreference.scala ├── com └── github │ └── shadowsocks │ ├── AboutFragment.scala │ ├── AppManager.scala │ ├── BaseService.scala │ ├── BootReceiver.scala │ ├── GlobalConfigFragment.scala │ ├── GlobalSettingsFragment.scala │ ├── GuardedProcess.scala │ ├── MainActivity.scala │ ├── ProfileConfigActivity.scala │ ├── ProfileConfigFragment.scala │ ├── ProfilesFragment.scala │ ├── QRCodeDialog.scala │ ├── QuickToggleShortcut.scala │ ├── ServiceBoundContext.scala │ ├── ShadowsocksApplication.scala │ ├── ShadowsocksBackupAgent.scala │ ├── ShadowsocksNatService.scala │ ├── ShadowsocksNotification.scala │ ├── ShadowsocksRunnerActivity.scala │ ├── ShadowsocksRunnerService.scala │ ├── ShadowsocksTileService.scala │ ├── ShadowsocksVpnService.scala │ ├── ShadowsocksVpnThread.scala │ ├── TaskerActivity.scala │ ├── TaskerReceiver.scala │ ├── ToolbarFragment.scala │ ├── acl │ ├── Acl.scala │ ├── AclSyncJob.scala │ ├── CustomRulesFragment.scala │ ├── DonaldTrump.scala │ └── Subnet.scala │ ├── database │ ├── DBHelper.scala │ ├── Profile.scala │ └── ProfileManager.scala │ ├── preferences │ └── KcpCliPreferenceDialogFragment.scala │ ├── utils │ ├── CloseUtils.scala │ ├── Constants.scala │ ├── IOUtils.scala │ ├── Parser.scala │ ├── TaskerSettings.scala │ ├── TcpFastOpen.scala │ ├── TrafficMonitor.scala │ ├── TrafficMonitorThread.scala │ ├── Typefaces.scala │ └── Utils.scala │ └── widget │ ├── BoundedCardView.scala │ ├── BoundedGridLayout.scala │ ├── BoundedView.scala │ └── UndoSnackbarManager.scala └── scala └── collection └── mutable └── SortedList.scala /.github/faq.md: -------------------------------------------------------------------------------- 1 | ### Troubleshooting 2 | 3 | Cannot connect to server: 4 | 5 | 1. Stop battery saver if it's active; 6 | 2. If an upgrade breaks anything, do a manual reset first by pressing Reset at the end of the list; 7 | 3. Check your config; 8 | 4. Wipe app data. 9 | 10 | Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issues/new) with logcat attached, or submit a crash report to Google Play. Then, try wiping app data. 11 | 12 | ### UI tips 13 | 14 | * Tap the number to enter the port you wish to use; (if the keyboard doesn't pop up automatically for some reason) 15 | * Use Tasker integration to create a desktop widget. 16 | 17 | ### Why is NAT mode deprecated? 18 | 19 | 1. Requiring ROOT permission; 20 | 2. No IPv6 support; 21 | 3. No UDP relay support. 22 | 23 | ### How to remove the exclamation mark when using VPN mode? 24 | 25 | The exclamation mark in the Wi-Fi/cellular icon appears because the system fails to connect to portal server (defaults to `clients3.google.com`) without VPN connection. To remove it, follow the instructions in [this article](https://www.noisyfox.cn/45.html). (in Simplified Chinese) 26 | 27 | ### Why are MIUI, EMUI and other AOSPs in China not officially supported? 28 | 29 | 1. Broken VPNService implementation, especially for IPv6; 30 | 2. Aggressive (or called broken) background service killing policy. 31 | 32 | Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) 33 | 34 | ### How to pause Shadowsocks service? 35 | 36 | * For Android 7.0+: Use quick switch tile in Quick Settings; 37 | * Use Tasker integration; 38 | * Add a profile with per-app proxy enabled for Shadowsocks only, bypass mode off. 39 | 40 | ### Why does Shadowsocks consume so much battery on Android 5.0+? 41 | 42 | As Shadowsocks takes over the whole device network, any battery used by network activities from other apps are also counted as those from Shadowsocks. So, the battery usage of Shadowsocks equals to the sum of all the network activities of your device. Shadowsocks itself is a totally I/O bound application on modern Android devices, which is expected not to consume any notable battery. 43 | 44 | So if you notice a significant increase in battery usage after you use Shadowsocks, it's most likely caused by other apps. For example, Google Play services can consume more battery after being able to connecting to Google, etc. 45 | 46 | ### It works fine under Wi-Fi but can't connect through cellular data? 47 | 48 | Allow this app to consume background data in app settings. 49 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Please read FAQ then answer these questions before submitting your issue. Thanks! 2 | 3 | ### Environment 4 | 5 | * Android version: *e.g. "7.0.0_r14", more detailed description is preferred* 6 | * Device: *e.g. Google Pixel XL* 7 | * Shadowsocks version: *version code "v3.0.0" or commit ID "a073f85"* 8 | * Last version that did not exhibit the issue (if applicable): 9 | 10 | ### Configuration 11 | 12 | _Put an `x` inside the [ ] that applies._ 13 | 14 | * [ ] IPv4 server address 15 | * [ ] IPv6 server address 16 | * [ ] Client IPv4 availability 17 | * [ ] Client IPv6 availability 18 | * Local port: 1080 19 | * Encrypt method: 20 | * [ ] One-time authentication 21 | * Route 22 | * [ ] All 23 | * [ ] Bypass LAN 24 | * [ ] Bypass China 25 | * [ ] Bypass LAN & China 26 | * [ ] GFW List 27 | * [ ] China List 28 | * [ ] Custom rules 29 | * [ ] IPv6 route 30 | * [ ] Per-App Proxy 31 | * [ ] Bypass mode 32 | * [ ] UDP Forwarding 33 | * KCP Parameters: (leave blank if not enabled) 34 | * [ ] Auto Connect 35 | * [ ] TCP Fast Open 36 | * [ ] NAT mode 37 | 38 | ### What did you do? 39 | 40 | 41 | ### What did you expect to see? 42 | 43 | 44 | ### What did you see instead? 45 | 46 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Type of changes 2 | 3 | _Put an `x` inside the [ ] that applies._ 4 | 5 | * [ ] Bugfix 6 | * [ ] New feature 7 | * [ ] Translation 8 | * [ ] General refinement 9 | * Other: 10 | 11 | ### Details 12 | 13 | Please provide more details about changes if necessary. 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | classes 2 | bin 3 | gen 4 | target 5 | local.properties 6 | .classpath 7 | .project 8 | .settings 9 | tests/bin 10 | tests/gen 11 | tests/local.properties 12 | NUL 13 | gen_* 14 | *.class 15 | *.o 16 | local.sbt 17 | .deps 18 | /proguard-sbt.txt 19 | 20 | #Intellij IDEA 21 | *.iml 22 | *.ipr 23 | *.iws 24 | .idea/ 25 | out/ 26 | .d 27 | 28 | #NDK 29 | src/main/obj 30 | src/main/libs 31 | src/main/assets/armeabi-v7a/* 32 | src/main/assets/x86/* 33 | jni/Application.mk 34 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/main/jni/shadowsocks-libev"] 2 | path = src/main/jni/shadowsocks-libev 3 | url = https://github.com/kaneawk/shadowsocksr-libev.git 4 | [submodule "src/main/jni/badvpn"] 5 | path = src/main/jni/badvpn 6 | url = https://github.com/shadowsocks/badvpn.git 7 | branch = shadowsocks-android 8 | [submodule "src/main/jni/libancillary"] 9 | path = src/main/jni/libancillary 10 | url = https://github.com/shadowsocks/libancillary.git 11 | branch = shadowsocks-android 12 | [submodule "src/main/jni/libevent"] 13 | path = src/main/jni/libevent 14 | url = https://github.com/shadowsocks/libevent.git 15 | branch = shadowsocks-android 16 | [submodule "src/main/jni/redsocks"] 17 | path = src/main/jni/redsocks 18 | url = https://github.com/shadowsocks/redsocks.git 19 | branch = shadowsocks-android 20 | [submodule "src/main/jni/pdnsd"] 21 | path = src/main/jni/pdnsd 22 | url = https://github.com/shadowsocks/pdnsd.git 23 | branch = shadowsocks-android 24 | [submodule "kcptun/kcptun"] 25 | path = kcptun/kcptun 26 | url = https://github.com/shadowsocks/kcptun 27 | [submodule "kcptun/go"] 28 | path = kcptun/go 29 | url = https://github.com/shadowsocks/go 30 | [submodule "src/main/jni/mbedtls"] 31 | path = src/main/jni/mbedtls 32 | url = https://github.com/ARMmbed/mbedtls 33 | [submodule "src/main/jni/pcre"] 34 | path = src/main/jni/pcre 35 | url = https://android.googlesource.com/platform/external/pcre 36 | -------------------------------------------------------------------------------- /.headache.cfg: -------------------------------------------------------------------------------- 1 | # Scala source 2 | ".*\\.scala" -> frame open:"/*" line:"*" close:"*/" 3 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -XX:MaxPermSize=512m 2 | -Xms1g 3 | -Xmx3g 4 | -Xss2m 5 | -XX:+CMSClassUnloadingEnabled 6 | -XX:+UseConcMarkSweepGC 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | env: 7 | global: 8 | - NDK_VERSION=r12b 9 | - NDK_CCACHE=ccache 10 | - GOROOT_BOOTSTRAP=$GOROOT 11 | - ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION} 12 | - SBTPATH=$HOME/.sbt 13 | - PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} 14 | 15 | scala: 16 | - 2.11.8 17 | 18 | before_cache: 19 | - find $HOME/.sbt -name "*.lock" | xargs rm 20 | - find $HOME/.ivy2 -name "*.lock" | xargs rm 21 | 22 | cache: 23 | directories: 24 | - $HOME/.ivy2 25 | - $HOME/.sbt 26 | 27 | android: 28 | components: 29 | - tools 30 | - build-tools-25.0.2 31 | - extra-android-m2repository 32 | - extra-google-m2repository 33 | 34 | install: 35 | - > 36 | if [ ! -d "$ANDROID_NDK_HOME" ]; then 37 | mkdir -p $ANDROID_NDK_HOME; 38 | pushd $HOME/.android; 39 | export ARCH=`uname -m`; 40 | wget -q http://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux-${ARCH}.zip; 41 | unzip -q android-ndk-${NDK_VERSION}-linux-${ARCH}.zip; 42 | popd; 43 | fi 44 | - > 45 | if [ ! -f "$SBTPATH/sbt" ]; then 46 | pushd $SBTPATH; 47 | wget -q https://raw.githubusercontent.com/paulp/sbt-extras/master/sbt; 48 | chmod a+x sbt; 49 | popd; 50 | fi 51 | 52 | script: 53 | - $SBTPATH/sbt native-build android:package-debug 54 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Shadowsocks-android was originally created in late 2013, by 2 | Max Lv . 3 | 4 | Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- 5 | people who have submitted patches, fixed bugs, added translations, and 6 | generally made shadowsocks-android that much better: 7 | 8 | https://github.com/shadowsocks/shadowsocks-android/graphs/contributors 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/wiki/FAQ) before submitting new issues. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2017 by Max Lv 3 | Copyright (C) 2017 by Mygod Studio 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.11.8" 2 | dexMaxHeap := "4g" 3 | 4 | enablePlugins(AndroidApp) 5 | android.useSupportVectors 6 | 7 | name := "shadowsocksr" 8 | version := "3.4.2" 9 | versionCode := Some(174) 10 | applicationId := "net.htcp.shadowsocksr" 11 | 12 | platformTarget := "android-25" 13 | 14 | compileOrder := CompileOrder.JavaThenScala 15 | javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil 16 | scalacOptions ++= "-target:jvm-1.7" :: "-Xexperimental" :: Nil 17 | 18 | proguardVersion := "5.3.2" 19 | proguardCache := Seq() 20 | proguardOptions ++= 21 | "-keep class com.github.shadowsocks.System { *; }" :: 22 | "-dontwarn com.google.android.gms.internal.**" :: 23 | "-dontwarn com.j256.ormlite.**" :: 24 | "-dontwarn okio.**" :: 25 | "-dontwarn org.xbill.**" :: 26 | Nil 27 | 28 | shrinkResources := true 29 | typedResources := false 30 | resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW") 31 | 32 | val supportLibsVersion = "25.1.0" 33 | val playServicesVersion = "10.0.1" 34 | resolvers += Resolver.jcenterRepo 35 | libraryDependencies ++= 36 | "com.android.support" % "cardview-v7" % supportLibsVersion :: 37 | "com.android.support" % "customtabs" % supportLibsVersion :: 38 | "com.android.support" % "design" % supportLibsVersion :: 39 | "com.android.support" % "gridlayout-v7" % supportLibsVersion :: 40 | "com.android.support" % "preference-v14" % supportLibsVersion :: 41 | "com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" :: 42 | "com.evernote" % "android-job" % "1.1.4" :: 43 | "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: 44 | "com.google.android.gms" % "play-services-analytics" % playServicesVersion :: 45 | "com.google.android.gms" % "play-services-gcm" % playServicesVersion :: 46 | "com.j256.ormlite" % "ormlite-android" % "5.0" :: 47 | "com.mikepenz" % "crossfader" % "1.5.0" :: 48 | "com.mikepenz" % "fastadapter" % "2.1.5" :: 49 | "com.mikepenz" % "iconics-core" % "2.8.2" :: 50 | "com.mikepenz" % "materialdrawer" % "5.8.1" :: 51 | "com.mikepenz" % "materialize" % "1.0.0" :: 52 | "com.squareup.okhttp3" % "okhttp" % "3.5.0" :: 53 | "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: 54 | "dnsjava" % "dnsjava" % "2.1.7" :: 55 | "eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" :: 56 | "net.glxn.qrgen" % "android" % "2.0" :: 57 | Nil 58 | 59 | lazy val nativeBuild = TaskKey[Unit]("native-build", "Build native executables") 60 | nativeBuild := { 61 | val logger = streams.value.log 62 | Process("./build.sh") ! logger match { 63 | case 0 => // Success! 64 | case n => sys.error(s"Native build script exit code: $n") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function try () { 4 | "$@" || exit -1 5 | } 6 | 7 | pushd src/main 8 | # Clean up old binaries (no longer used) 9 | rm -rf assets/armeabi-v7a assets/x86 libs/armeabi-v7a libs/x86 10 | mkdir -p libs/armeabi-v7a libs/x86 11 | popd 12 | 13 | # Build kcptun 14 | pushd kcptun 15 | try ./make.bash 16 | popd 17 | -------------------------------------------------------------------------------- /gfwlist/gen.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | ## ArchLinux install package via pacman: perl-net-cidr-lite 3 | use strict; 4 | use warnings; 5 | use Net::CIDR::Lite; 6 | my $cidr = Net::CIDR::Lite->new; 7 | while (my $line=<>) { 8 | $cidr->add($line); 9 | } 10 | foreach my $line( @{$cidr->list} ) { 11 | print "$line\n"; 12 | } 13 | -------------------------------------------------------------------------------- /gfwlist/gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- encoding: utf8 -*- 3 | 4 | import itertools 5 | import math 6 | import sys 7 | 8 | import IPy 9 | 10 | 11 | def main(): 12 | china_list_set = IPy.IPSet() 13 | for line in sys.stdin: 14 | china_list_set.add(IPy.IP(line)) 15 | 16 | # 输出结果 17 | for ip in china_list_set: 18 | print '' + str(ip) + '' 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /kcptun/make.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function try () { 4 | "$@" || exit -1 5 | } 6 | 7 | [ -z "$ANDROID_NDK_HOME" ] && ANDROID_NDK_HOME=~/android-ndk-r12b 8 | 9 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | DEPS=$DIR/.deps 11 | ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-16-arm 12 | ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-16-x86 13 | 14 | ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-gcc 15 | ANDROID_ARM_STRIP=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-strip 16 | 17 | ANDROID_X86_CC=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-gcc 18 | ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip 19 | 20 | 21 | if [ ! -d "$DEPS" ]; then 22 | mkdir -p $DEPS 23 | fi 24 | 25 | if [ ! -d "$ANDROID_ARM_TOOLCHAIN" ]; then 26 | echo "Make standalone toolchain for ARM arch" 27 | $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm \ 28 | --api 16 --install-dir $ANDROID_ARM_TOOLCHAIN 29 | fi 30 | 31 | if [ ! -d "$ANDROID_X86_TOOLCHAIN" ]; then 32 | echo "Make standalone toolchain for X86 arch" 33 | $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch x86 \ 34 | --api 16 --install-dir $ANDROID_X86_TOOLCHAIN 35 | fi 36 | 37 | if [ ! -d "$DIR/go/bin" ]; then 38 | echo "Build the custom go" 39 | 40 | pushd $DIR/go/src 41 | try ./make.bash 42 | popd 43 | fi 44 | 45 | export GOROOT=$DIR/go 46 | export GOPATH=$DEPS/gopath 47 | export GOBIN=$GOPATH/bin 48 | mkdir -p $GOBIN 49 | export PATH=$GOROOT/bin:$PATH 50 | 51 | pushd kcptun/client 52 | 53 | echo "Get dependences for kcptun" 54 | go get -u github.com/xtaci/kcp-go 55 | go get -u github.com/xtaci/smux 56 | go get 57 | 58 | echo "Cross compile kcptun for arm" 59 | try env CGO_ENABLED=1 CC=$ANDROID_ARM_CC GOOS=android GOARCH=arm GOARM=7 go build -ldflags="-s -w" 60 | try $ANDROID_ARM_STRIP client 61 | try mv client $DIR/../src/main/libs/armeabi-v7a/libkcptun.so 62 | 63 | echo "Cross compile kcptun for x86" 64 | try env CGO_ENABLED=1 CC=$ANDROID_X86_CC GOOS=android GOARCH=386 go build -ldflags="-s -w" 65 | try $ANDROID_X86_STRIP client 66 | try mv client $DIR/../src/main/libs/x86/libkcptun.so 67 | popd 68 | 69 | echo "Successfully build kcptun" 70 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.2") 2 | 3 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10") 4 | 5 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.8.2") 6 | -------------------------------------------------------------------------------- /shadow-notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/shadow-notify.png -------------------------------------------------------------------------------- /shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/shadow.png -------------------------------------------------------------------------------- /src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl: -------------------------------------------------------------------------------- 1 | package com.github.shadowsocks.aidl; 2 | 3 | import com.github.shadowsocks.aidl.IShadowsocksServiceCallback; 4 | 5 | interface IShadowsocksService { 6 | int getState(); 7 | String getProfileName(); 8 | 9 | oneway void registerCallback(IShadowsocksServiceCallback cb); 10 | oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb); 11 | oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb); 12 | oneway void unregisterCallback(IShadowsocksServiceCallback cb); 13 | 14 | oneway void use(in int profileId); 15 | void useSync(in int profileId); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | package com.github.shadowsocks.aidl; 2 | 3 | interface IShadowsocksServiceCallback { 4 | oneway void stateChanged(int state, String profileName, String msg); 5 | oneway void trafficUpdated(int profileId, long txRate, long rxRate, long txTotal, long rxTotal); 6 | // Traffic data has persisted to database, listener should refetch their data from database 7 | oneway void trafficPersisted(int profileId); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/assets/acl/bypass-lan.acl: -------------------------------------------------------------------------------- 1 | 0.0.0.0/8 2 | 10.0.0.0/8 3 | 100.64.0.0/10 4 | 127.0.0.0/8 5 | 169.254.0.0/16 6 | 172.16.0.0/12 7 | 192.0.0.0/29 8 | 192.0.2.0/24 9 | 192.88.99.0/24 10 | 192.168.0.0/16 11 | 198.18.0.0/15 12 | 198.51.100.0/24 13 | 203.0.113.0/24 14 | 224.0.0.0/3 15 | -------------------------------------------------------------------------------- /src/main/assets/fonts/Iceland.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/assets/fonts/Iceland.ttf -------------------------------------------------------------------------------- /src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /src/main/java/com/github/shadowsocks/DialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.shadowsocks; 2 | 3 | import android.app.Activity; 4 | 5 | /** 6 | * This helper class is used to solve Java-Scala interop problem. Use onAttach(Context) and remove this class 7 | * when minSdkVersion >= 23. 8 | * 9 | * @author Mygod 10 | */ 11 | public class DialogFragment extends android.app.DialogFragment { 12 | protected void superOnAttach(Activity activity) { 13 | super.onAttach(activity); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/shadowsocks/System.java: -------------------------------------------------------------------------------- 1 | /* Shadowsocks - A shadowsocks client for Android 2 | * Copyright (C) 2012 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | * 18 | * ___====-_ _-====___ 19 | * _--^^^#####// \\#####^^^--_ 20 | * _-^##########// ( ) \\##########^-_ 21 | * -############// |\^^/| \\############- 22 | * _/############// (@::@) \\############\_ 23 | * /#############(( \\// ))#############\ 24 | * -###############\\ (oo) //###############- 25 | * -#################\\ / VV \ //#################- 26 | * -###################\\/ \//###################- 27 | * _#/|##########/\######( /\ )######/\##########|\#_ 28 | * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \| 29 | * ` |/ V V ` V \#\| | | |/#/ V ' V V \| ' 30 | * ` ` ` ` / | | | | \ ' ' ' ' 31 | * ( | | | | ) 32 | * __\ | | | | /__ 33 | * (vvv(VVV)(VVV)vvv) 34 | * 35 | * HERE BE DRAGONS 36 | * 37 | */ 38 | 39 | package com.github.shadowsocks; 40 | 41 | public class System { 42 | static { 43 | java.lang.System.loadLibrary("system"); 44 | } 45 | 46 | public static native int exec(String cmd); 47 | public static native int sendfd(int fd, String path); 48 | public static native void jniclose(int fd); 49 | } 50 | -------------------------------------------------------------------------------- /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/jni/build-shared-executable.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2009 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # this file is included from Android.mk files to build a target-specific 16 | # executable program 17 | # 18 | # Modified by @Mygod, based on: 19 | # https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-shared-library.mk 20 | # https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-executable.mk 21 | LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE 22 | LOCAL_MAKEFILE := $(local-makefile) 23 | $(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT)) 24 | $(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE)) 25 | $(call check-LOCAL_MODULE_FILENAME) 26 | # we are building target objects 27 | my := TARGET_ 28 | $(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION)) 29 | $(call handle-module-built) 30 | LOCAL_MODULE_CLASS := EXECUTABLE 31 | include $(BUILD_SYSTEM)/build-module.mk 32 | -------------------------------------------------------------------------------- /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/jni/include/sodium/version.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef sodium_version_H 3 | #define sodium_version_H 4 | 5 | #include "export.h" 6 | 7 | #define SODIUM_VERSION_STRING "1.0.7" 8 | 9 | #define SODIUM_LIBRARY_VERSION_MAJOR 9 10 | #define SODIUM_LIBRARY_VERSION_MINOR 0 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | SODIUM_EXPORT 17 | const char *sodium_version_string(void); 18 | 19 | SODIUM_EXPORT 20 | int sodium_library_version_major(void); 21 | 22 | SODIUM_EXPORT 23 | int sodium_library_version_minor(void); 24 | 25 | #ifdef __cplusplus 26 | } 27 | #endif 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /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/res/drawable-anydpi-v21/ic_navigation_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/drawable-mdpi/ic_stat_shadowsocks.png -------------------------------------------------------------------------------- /src/main/res/drawable-v21/background_selectable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res/drawable-v21/background_stat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/ic_navigation_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/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/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/drawable-xxxhdpi/ic_navigation_close.png -------------------------------------------------------------------------------- /src/main/res/drawable/background_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/drawable/background_header.png -------------------------------------------------------------------------------- /src/main/res/drawable/background_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_selectable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/res/drawable/background_stat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_assignment.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_copyright.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_description.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_done.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_help_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_note_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_action_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_av_playlist_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_content_copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_content_paste.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_image_edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_navigation_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_qu_shadowsocks_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_social_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_start_busy.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_start_connected.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_start_idle.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/res/layout/dialog_acl_rule.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_apps.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 28 | 39 | 40 | 43 | 47 | 52 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_apps_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 18 | 19 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_custom_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 16 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_global_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 11 | 12 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /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/res/layout/layout_profile_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/res/layout/layout_tasker.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/res/layout/toolbar_light_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /src/main/res/menu/app_manager_menu.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/menu/custom_rules_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 17 | 22 | 27 | 32 | 33 | 34 | 39 | 40 | 44 | 48 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/res/menu/profile_config_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/res/menu/profile_manager_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 14 | 18 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/res/menu/profile_share_popup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/raw/gtm_default_container: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneawk/shadowsocksr-android/37728bb334d2443fd64c728a6f0708f577cb04cb/src/main/res/raw/gtm_default_container -------------------------------------------------------------------------------- /src/main/res/values-v21/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -28dp 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/values-v21/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @string/proxied_apps_summary_v21 4 | 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/res/xml/pref_global.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/xml/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res/xml/tracker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 604800 5 | 6 | 7 | true 8 | 9 | 10 | true 11 | 12 | 13 | 14 | Shadowsocks ScreenView 15 | 16 | 17 | 18 | Shadowsocks EcommerceView 19 | 20 | 21 | 22 | UA-37082941-1 23 | 24 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/DialogPreferencePlus.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.app.DialogFragment 24 | import android.support.v7.preference.DialogPreference 25 | 26 | trait DialogPreferencePlus extends DialogPreference { 27 | def createDialog(): DialogFragment 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/EditTextPreference.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.support.v7.preference.{EditTextPreference => Parent} 25 | import android.support.v7.widget.AppCompatEditText 26 | import android.text.InputType 27 | import android.util.AttributeSet 28 | 29 | /** 30 | * Fixed EditTextPreference + SummaryPreference with password support! 31 | * Based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/tree/master/app/src/main/java/android/support/v7/preference 32 | */ 33 | class EditTextPreference(context: Context, attrs: AttributeSet = null) extends Parent(context, attrs) 34 | with DialogPreferencePlus with SummaryPreference { 35 | val editText = new AppCompatEditText(context, attrs) 36 | editText.setId(android.R.id.edit) 37 | 38 | override def createDialog() = new EditTextPreferenceDialogFragment() 39 | 40 | override protected def getSummaryValue: String = { 41 | var text = getText 42 | if (text == null) text = "" 43 | val inputType = editText.getInputType 44 | if (inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) || 45 | inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) || 46 | inputType == (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD)) 47 | "\u2022" * text.length else text 48 | } 49 | 50 | override def setText(text: String): Unit = { 51 | super.setText(text) 52 | notifyChanged() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.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.support.v14.preference.PreferenceDialogFragment 25 | import android.support.v7.widget.AppCompatEditText 26 | import android.view.{View, ViewGroup} 27 | 28 | class EditTextPreferenceDialogFragment extends PreferenceDialogFragment { 29 | private lazy val preference = getPreference.asInstanceOf[EditTextPreference] 30 | private lazy val editText = preference.editText 31 | 32 | override protected def onCreateDialogView(context: Context): AppCompatEditText = { 33 | val parent = editText.getParent.asInstanceOf[ViewGroup] 34 | if (parent != null) parent.removeView(editText) 35 | editText 36 | } 37 | 38 | override protected def onBindDialogView(view: View) { 39 | super.onBindDialogView(view) 40 | editText.setText(preference.getText) 41 | val text = editText.getText 42 | if (text != null) editText.setSelection(0, text.length) 43 | } 44 | 45 | override protected def needInputMethod = true 46 | 47 | def onDialogClosed(positiveResult: Boolean): Unit = if (positiveResult) { 48 | val value = editText.getText.toString 49 | if (preference.callChangeListener(value)) preference.setText(value) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/be/mygod/preference/NumberPickerPreferenceDialogFragment.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.support.v14.preference.PreferenceDialogFragment 25 | import android.view.{View, ViewGroup} 26 | import android.widget.NumberPicker 27 | 28 | class NumberPickerPreferenceDialogFragment extends PreferenceDialogFragment { 29 | private lazy val preference = getPreference.asInstanceOf[NumberPickerPreference] 30 | private lazy val picker = preference.picker 31 | 32 | override protected def onCreateDialogView(context: Context): NumberPicker = { 33 | val parent = picker.getParent.asInstanceOf[ViewGroup] 34 | if (parent != null) parent.removeView(picker) 35 | picker 36 | } 37 | 38 | override protected def onBindDialogView(view: View) { 39 | super.onBindDialogView(view) 40 | picker.setValue(preference.getValue) 41 | } 42 | 43 | override protected def needInputMethod = true 44 | 45 | def onDialogClosed(positiveResult: Boolean) { 46 | picker.clearFocus() // commit changes 47 | if (positiveResult) { 48 | val value = picker.getValue 49 | if (preference.callChangeListener(value)) preference.setValue(value) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/PreferenceCategory.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.support.v7.preference.{PreferenceViewHolder, PreferenceCategory => Base} 25 | import android.util.AttributeSet 26 | import android.view.ViewGroup.MarginLayoutParams 27 | 28 | /** 29 | * Based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/blob/4c2bb2896895dbedb37b659b36bd2a96b33c1605/preference-v7/src/main/java/com/takisoft/fix/support/v7/preference/PreferenceCategory.java 30 | * 31 | * @author Mygod 32 | */ 33 | class PreferenceCategory(context: Context, attrs: AttributeSet = null) extends Base(context, attrs) { 34 | override def onBindViewHolder(holder: PreferenceViewHolder) { 35 | super.onBindViewHolder(holder) 36 | holder.findViewById(android.R.id.title).getLayoutParams.asInstanceOf[MarginLayoutParams].bottomMargin = 0 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/be/mygod/preference/PreferenceFragment.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.app.DialogFragment 24 | import android.os.Bundle 25 | import android.support.v14.preference.{PreferenceFragment => Base} 26 | import android.support.v7.preference.{Preference, PreferenceScreen} 27 | import android.view.{LayoutInflater, View, ViewGroup} 28 | 29 | abstract class PreferenceFragment extends Base { 30 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = 31 | super.onCreateView(inflater, container, savedInstanceState) 32 | 33 | protected final def displayPreferenceDialog(key: String, fragment: DialogFragment) { 34 | val bundle = new Bundle(1) 35 | bundle.putString("key", key) 36 | fragment.setArguments(bundle) 37 | fragment.setTargetFragment(this, 0) 38 | getFragmentManager.beginTransaction() 39 | .add(fragment, "android.support.v14.preference.PreferenceFragment.DIALOG") 40 | .commitAllowingStateLoss() 41 | } 42 | 43 | override def onDisplayPreferenceDialog(preference: Preference): Unit = preference match { 44 | case dpp: DialogPreferencePlus => displayPreferenceDialog(preference.getKey, dpp.createDialog()) 45 | case _ => super.onDisplayPreferenceDialog(preference) 46 | } 47 | 48 | override protected def onCreateAdapter(screen: PreferenceScreen) = new PreferenceGroupAdapter(screen) 49 | 50 | override def onResume() { 51 | super.onResume() 52 | getListView.scrollBy(0, 0) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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/be/mygod/preference/SummaryPreference.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.support.v7.preference.Preference 24 | 25 | /** 26 | * Make your preference support %s in summary. Override getSummaryValue to customize what to put in. 27 | * Based on: 28 | * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/preference/ListPreference.java 29 | * 30 | * @author Mygod 31 | */ 32 | trait SummaryPreference extends Preference { 33 | protected def getSummaryValue: AnyRef 34 | 35 | /** 36 | * Returns the summary of this SummaryPreference. If the summary has a String formatting marker in it 37 | * (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place. 38 | * 39 | * @return the summary with appropriate string substitution 40 | */ 41 | override def getSummary: String = { 42 | val summary = super.getSummary 43 | if (summary == null) null else String.format(summary.toString, getSummaryValue) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/AboutFragment.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 | import java.util.Locale 23 | 24 | import android.content.Intent 25 | import android.net.Uri 26 | import android.os.Bundle 27 | import android.view.{LayoutInflater, View, ViewGroup} 28 | import android.webkit.{WebView, WebViewClient} 29 | 30 | class AboutFragment extends ToolbarFragment { 31 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = 32 | inflater.inflate(R.layout.layout_about, container, false) 33 | 34 | override def onViewCreated(view: View, savedInstanceState: Bundle) { 35 | super.onViewCreated(view, savedInstanceState) 36 | toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME)) 37 | val web = view.findViewById(R.id.web_view).asInstanceOf[WebView] 38 | web.loadUrl("file:///android_asset/pages/about.html") 39 | web.setWebViewClient(new WebViewClient() { 40 | override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = { 41 | getActivity.asInstanceOf[MainActivity].launchUrl(url) 42 | true 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/BootReceiver.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.content.pm.PackageManager 24 | import android.content.{BroadcastReceiver, ComponentName, Context, Intent} 25 | import com.github.shadowsocks.utils._ 26 | 27 | object BootReceiver { 28 | def getEnabled(context: Context): Boolean = PackageManager.COMPONENT_ENABLED_STATE_ENABLED == 29 | context.getPackageManager.getComponentEnabledSetting(new ComponentName(context, classOf[BootReceiver])) 30 | def setEnabled(context: Context, enabled: Boolean): Unit = context.getPackageManager.setComponentEnabledSetting( 31 | new ComponentName(context, classOf[BootReceiver]), 32 | if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 33 | PackageManager.DONT_KILL_APP) 34 | } 35 | 36 | class BootReceiver extends BroadcastReceiver { 37 | def onReceive(context: Context, intent: Intent): Unit = { 38 | Utils.startSsService(context) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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/GlobalSettingsFragment.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.view.{LayoutInflater, View, ViewGroup} 25 | import android.app.Fragment 26 | 27 | class GlobalSettingsFragment extends ToolbarFragment { 28 | 29 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = 30 | inflater.inflate(R.layout.layout_global_settings, container, false) 31 | 32 | override def onViewCreated(view: View, savedInstanceState: Bundle) { 33 | super.onViewCreated(view, savedInstanceState) 34 | toolbar.setTitle(R.string.settings) 35 | 36 | val fm = getChildFragmentManager 37 | fm.beginTransaction().replace(R.id.content, new GlobalConfigFragment()).commit() 38 | fm.executePendingTransactions() 39 | } 40 | 41 | override def onDetach() { 42 | super.onDetach() 43 | 44 | try { 45 | val childFragmentManager = classOf[Fragment].getDeclaredField("mChildFragmentManager") 46 | childFragmentManager.setAccessible(true) 47 | childFragmentManager.set(this, null) 48 | } catch { 49 | case _: Exception => // ignore 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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/com/github/shadowsocks/ProfileConfigActivity.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 24 | import android.content.DialogInterface 25 | import android.os.Bundle 26 | import android.support.v7.app.AlertDialog 27 | import android.support.v7.widget.Toolbar 28 | import com.github.shadowsocks.ShadowsocksApplication.app 29 | import com.github.shadowsocks.utils.Key 30 | 31 | class ProfileConfigActivity extends Activity { 32 | private lazy val child = getFragmentManager.findFragmentById(R.id.content).asInstanceOf[ProfileConfigFragment] 33 | 34 | override def onCreate(savedInstanceState: Bundle) { 35 | super.onCreate(savedInstanceState) 36 | setContentView(R.layout.layout_profile_config) 37 | val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] 38 | toolbar.setTitle(R.string.profile_config) 39 | toolbar.setNavigationIcon(R.drawable.ic_navigation_close) 40 | toolbar.setNavigationOnClickListener(_ => onBackPressed()) 41 | toolbar.inflateMenu(R.menu.profile_config_menu) 42 | toolbar.setOnMenuItemClickListener(child) 43 | } 44 | 45 | override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this) 46 | .setTitle(R.string.unsaved_changes_prompt) 47 | .setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener) 48 | .setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener) 49 | .setNeutralButton(android.R.string.cancel, null) 50 | .create() 51 | .show() else super.onBackPressed() 52 | } 53 | -------------------------------------------------------------------------------- /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/com/github/shadowsocks/QuickToggleShortcut.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 24 | import android.content.Intent 25 | import android.content.pm.ShortcutManager 26 | import android.os.{Build, Bundle} 27 | import com.github.shadowsocks.utils.{State, Utils} 28 | 29 | /** 30 | * @author Mygod 31 | */ 32 | class QuickToggleShortcut extends Activity with ServiceBoundContext { 33 | override def onCreate(savedInstanceState: Bundle) { 34 | super.onCreate(savedInstanceState) 35 | getIntent.getAction match { 36 | case Intent.ACTION_CREATE_SHORTCUT => 37 | setResult(Activity.RESULT_OK, new Intent() 38 | .putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut])) 39 | .putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle)) 40 | .putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 41 | Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher))) 42 | finish() 43 | case _ => 44 | attachService() 45 | if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("toggle") 46 | } 47 | } 48 | 49 | override def onDestroy() { 50 | detachService() 51 | super.onDestroy() 52 | } 53 | 54 | override def onServiceConnected() { 55 | bgService.getState match { 56 | case State.STOPPED => Utils.startSsService(this) 57 | case State.CONNECTED => Utils.stopSsService(this) 58 | case _ => // ignore 59 | } 60 | finish() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.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.backup.{BackupAgentHelper, FileBackupHelper, SharedPreferencesBackupHelper} 24 | import com.github.shadowsocks.acl.Acl 25 | import com.github.shadowsocks.database.DBHelper 26 | 27 | class ShadowsocksBackupAgent extends BackupAgentHelper { 28 | 29 | // The names of the SharedPreferences groups that the application maintains. These 30 | // are the same strings that are passed to getSharedPreferences(String, int). 31 | val PREFS_DISPLAY = "com.github.shadowsocks_preferences" 32 | 33 | // An arbitrary string used within the BackupAgentHelper implementation to 34 | // identify the SharedPreferencesBackupHelper's data. 35 | val MY_PREFS_BACKUP_KEY = "com.github.shadowsocks" 36 | 37 | val DATABASE = "com.github.shadowsocks.database.profile" 38 | 39 | override def onCreate() { 40 | val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY) 41 | addHelper(MY_PREFS_BACKUP_KEY, helper) 42 | addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, 43 | "../" + Acl.CUSTOM_RULES + ".acl")) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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/ShadowsocksRunnerService.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.Service 24 | import android.content.Intent 25 | import android.net.VpnService 26 | import android.os.{Handler, IBinder} 27 | import com.github.shadowsocks.ShadowsocksApplication.app 28 | 29 | class ShadowsocksRunnerService extends Service with ServiceBoundContext { 30 | val handler = new Handler() 31 | 32 | override def onBind(intent: Intent): IBinder = { 33 | null 34 | } 35 | 36 | override def onServiceConnected() { 37 | handler.postDelayed(() => { 38 | if (bgService != null) { 39 | if (app.isNatEnabled) startBackgroundService() 40 | else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService() 41 | handler.postDelayed(() => stopSelf(), 3000) 42 | } 43 | }, 1000) 44 | } 45 | 46 | def startBackgroundService(): Unit = bgService.useSync(app.profileId) 47 | 48 | override def onCreate() { 49 | super.onCreate() 50 | attachService() 51 | } 52 | 53 | override def onDestroy() { 54 | super.onDestroy() 55 | detachService() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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/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/TaskerReceiver.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.content.{BroadcastReceiver, Context, Intent} 24 | import com.github.shadowsocks.utils.{TaskerSettings, Utils} 25 | import com.github.shadowsocks.ShadowsocksApplication.app 26 | 27 | /** 28 | * @author CzBiX 29 | */ 30 | class TaskerReceiver extends BroadcastReceiver { 31 | override def onReceive(context: Context, intent: Intent) { 32 | val settings = TaskerSettings.fromIntent(intent) 33 | app.profileManager.getProfile(settings.profileId) match { 34 | case Some(_) => app.switchProfile(settings.profileId) 35 | case _ => 36 | } 37 | if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/ToolbarFragment.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.Fragment 24 | import android.os.Bundle 25 | import android.support.v7.widget.Toolbar 26 | import android.view.View 27 | 28 | class ToolbarFragment extends Fragment { 29 | var toolbar: Toolbar = _ 30 | 31 | override def onViewCreated(view: View, savedInstanceState: Bundle) { 32 | super.onViewCreated(view, savedInstanceState) 33 | toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar] 34 | val activity = getActivity.asInstanceOf[MainActivity] 35 | if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true) 36 | } 37 | 38 | def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = () 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/acl/AclSyncJob.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.acl 22 | 23 | import java.io.IOException 24 | import java.util.concurrent.TimeUnit 25 | 26 | import com.evernote.android.job.Job.{Params, Result} 27 | import com.evernote.android.job.{Job, JobRequest} 28 | import com.github.shadowsocks.ShadowsocksApplication.app 29 | import com.github.shadowsocks.utils.IOUtils 30 | 31 | import scala.io.Source 32 | 33 | /** 34 | * @author Mygod 35 | */ 36 | object AclSyncJob { 37 | final val TAG = "AclSyncJob" 38 | 39 | def schedule(route: String): Int = new JobRequest.Builder(AclSyncJob.TAG + ':' + route) 40 | .setExecutionWindow(1, TimeUnit.DAYS.toMillis(28)) 41 | .setRequirementsEnforced(true) 42 | .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) 43 | .setRequiresCharging(true) 44 | .setUpdateCurrent(true) 45 | .build().schedule() 46 | } 47 | 48 | class AclSyncJob(route: String) extends Job { 49 | override def onRunJob(params: Params): Result = { 50 | val filename = route + ".acl" 51 | try { 52 | //noinspection JavaAccessorMethodCalledAsEmptyParen 53 | IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename, 54 | Source.fromURL("https://shadowsocks.org/acl/android/v1/" + filename).mkString) 55 | Result.SUCCESS 56 | } catch { 57 | case e: IOException => 58 | e.printStackTrace() 59 | Result.RESCHEDULE 60 | case e: Exception => // unknown failures, probably shouldn't retry 61 | e.printStackTrace() 62 | Result.FAILURE 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/acl/DonaldTrump.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.acl 22 | 23 | import android.util.Log 24 | import com.evernote.android.job.JobCreator 25 | 26 | /** 27 | * “I create jobs all day long. 28 | * - Donald Trump, 2015 29 | * 30 | * Source: http://www.cnn.com/2015/09/24/politics/donald-trump-marco-rubio-foreign-policy/ 31 | * 32 | * @author !Mygod 33 | */ 34 | object DonaldTrump extends JobCreator { 35 | def create(tag: String): AclSyncJob = { 36 | val parts = tag.split(":") 37 | parts(0) match { 38 | case AclSyncJob.TAG => new AclSyncJob(parts(1)) 39 | case _ => 40 | Log.w("DonaldTrump", "Unknown job tag: " + tag) 41 | null 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/acl/Subnet.scala: -------------------------------------------------------------------------------- 1 | package com.github.shadowsocks.acl 2 | 3 | import java.net.{Inet4Address, Inet6Address, InetAddress} 4 | 5 | import com.github.shadowsocks.utils.Utils 6 | 7 | /** 8 | * @author Mygod 9 | */ 10 | @throws[IllegalArgumentException] 11 | class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] { 12 | if (prefixSize < 0) throw new IllegalArgumentException 13 | address match { 14 | case _: Inet4Address => if (prefixSize > 32) throw new IllegalArgumentException 15 | case _: Inet6Address => if (prefixSize > 128) throw new IllegalArgumentException 16 | } 17 | 18 | override def toString: String = if (address match { 19 | case _: Inet4Address => prefixSize == 32 20 | case _: Inet6Address => prefixSize == 128 21 | }) address.getHostAddress else address.getHostAddress + '/' + prefixSize 22 | 23 | override def compareTo(that: Subnet): Int = { 24 | val addrThis = address.getAddress 25 | val addrThat = that.address.getAddress 26 | var result = addrThis lengthCompare addrThat.length // IPv4 address goes first 27 | if (result != 0) return result 28 | for ((x, y) <- addrThis zip addrThat) { 29 | result = (x & 0xFF) compare (y & 0xFF) // undo sign extension of signed byte 30 | if (result != 0) return result 31 | } 32 | prefixSize compare that.prefixSize 33 | } 34 | } 35 | 36 | object Subnet { 37 | @throws[IllegalArgumentException] 38 | def fromString(value: String): Subnet = { 39 | val parts = value.split("/") 40 | if (!Utils.isNumeric(parts(0))) throw new IllegalArgumentException() 41 | val addr = InetAddress.getByName(parts(0)) 42 | parts.length match { 43 | case 1 => new Subnet(addr, addr match { 44 | case _: Inet4Address => 32 45 | case _: Inet6Address => 128 46 | }) 47 | case 2 => new Subnet(addr, parts(1).toInt) 48 | case _ => throw new IllegalArgumentException() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/scala/com/github/shadowsocks/preferences/KcpCliPreferenceDialogFragment.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.preferences 22 | 23 | import android.app.AlertDialog 24 | import android.content.DialogInterface 25 | import eu.chainfire.libsuperuser.Shell 26 | import be.mygod.preference.EditTextPreferenceDialogFragment 27 | 28 | import scala.collection.JavaConverters._ 29 | 30 | /** 31 | * @author Mygod 32 | */ 33 | class KcpCliPreferenceDialogFragment extends EditTextPreferenceDialogFragment { 34 | override def onPrepareDialogBuilder(builder: AlertDialog.Builder) { 35 | super.onPrepareDialogBuilder(builder) 36 | builder.setNeutralButton("?", ((_, _) => new AlertDialog.Builder(builder.getContext) 37 | .setTitle("?") 38 | .setMessage(Shell.SH.run(builder.getContext.getApplicationInfo.nativeLibraryDir + "/libkcptun.so --help") 39 | .asScala 40 | .dropWhile(line => line != "GLOBAL OPTIONS:") 41 | .drop(1) 42 | .takeWhile(line => line.length() > 3) 43 | .filter(line => 44 | !line.startsWith(" --localaddr ") && 45 | !line.startsWith(" --remoteaddr ") && 46 | !line.startsWith(" --path ") && 47 | !line.startsWith(" --help,") && 48 | !line.startsWith(" --version,")) 49 | .mkString("\n") 50 | .replaceAll(" {2,}", "\n") 51 | .substring(1)) // remove 1st \n 52 | .show()): DialogInterface.OnClickListener) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/CloseUtils.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 | /** 24 | * @author Mygod 25 | */ 26 | object CloseUtils { 27 | type Disconnectable = { 28 | def disconnect() 29 | } 30 | 31 | def autoClose[A <: AutoCloseable, B](x: => A)(block: A => B): B = { 32 | var a: Option[A] = None 33 | try { 34 | a = Some(x) 35 | block(a.get) 36 | } finally if (a.nonEmpty) try { 37 | val v = a.get 38 | if (v ne null) v.close() 39 | } catch { 40 | case _: Exception => 41 | } 42 | } 43 | def autoDisconnect[A <: Disconnectable, B](x: => A)(block: A => B): B = { 44 | var a: Option[A] = None 45 | try { 46 | a = Some(x) 47 | block(a.get) 48 | } finally if (a.nonEmpty) try { 49 | val v = a.get 50 | if (v ne null) v.disconnect() 51 | } catch { 52 | case _: Exception => 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/IOUtils.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 com.github.shadowsocks.utils.CloseUtils._ 24 | import java.io.{FileWriter, InputStream, OutputStream} 25 | 26 | /** 27 | * @author Mygod 28 | */ 29 | object IOUtils { 30 | private final val BUFFER_SIZE = 32 * 1024 31 | 32 | def copy(in: InputStream, out: OutputStream) { 33 | val buffer = new Array[Byte](BUFFER_SIZE) 34 | while (true) { 35 | val count = in.read(buffer) 36 | if (count >= 0) out.write(buffer, 0, count) else return 37 | } 38 | } 39 | 40 | def writeString(file: String, content: String): Unit = 41 | autoClose(new FileWriter(file))(writer => writer.write(content)) 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/TaskerSettings.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 android.content.{Context, Intent} 24 | import android.os.Bundle 25 | import com.github.shadowsocks.R 26 | import com.github.shadowsocks.ShadowsocksApplication.app 27 | import com.twofortyfouram.locale.api.{Intent => ApiIntent} 28 | 29 | object TaskerSettings { 30 | private val KEY_SWITCH_ON = "switch_on" 31 | private val KEY_PROFILE_ID = "profile_id" 32 | 33 | def fromIntent(intent: Intent) = new TaskerSettings(if (intent.hasExtra(ApiIntent.EXTRA_BUNDLE)) 34 | intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE) else Bundle.EMPTY) 35 | } 36 | 37 | class TaskerSettings(bundle: Bundle) { 38 | import TaskerSettings._ 39 | 40 | var switchOn: Boolean = bundle.getBoolean(KEY_SWITCH_ON, true) 41 | var profileId: Int = bundle.getInt(KEY_PROFILE_ID, -1) 42 | 43 | def toIntent(context: Context): Intent = { 44 | val bundle = new Bundle() 45 | if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false) 46 | if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId) 47 | new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB, 48 | app.profileManager.getProfile(profileId) match { 49 | case Some(p) => context.getString(if (switchOn) R.string.start_service else R.string.stop_service, p.getName) 50 | case None => context.getString(if (switchOn) R.string.start_service_default else R.string.stop) 51 | }) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/utils/TcpFastOpen.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 24 | 25 | import eu.chainfire.libsuperuser.Shell 26 | 27 | import scala.collection.JavaConverters._ 28 | 29 | /** 30 | * @author Mygod 31 | */ 32 | object TcpFastOpen { 33 | /** 34 | * Is kernel version >= 3.7.1. 35 | */ 36 | lazy val supported: Boolean = "^(\\d+)\\.(\\d+)\\.(\\d+)".r.findFirstMatchIn(System.getProperty("os.version")) match { 37 | case Some(m) => 38 | val kernel = m.group(1).toInt 39 | if (kernel < 3) false else if (kernel > 3) true else { 40 | val major = m.group(2).toInt 41 | if (major < 7) false else if (major > 7) true else m.group(3).toInt >= 1 42 | } 43 | case _ => false 44 | } 45 | 46 | def sendEnabled: Boolean = { 47 | val file = new File("/proc/sys/net/ipv4/tcp_fastopen") 48 | file.canRead && (Utils.readAllLines(file).toInt & 1) > 0 49 | } 50 | 51 | def enabled(value: Boolean): String = if (sendEnabled != value) { 52 | val res = Shell.run("su", Array( 53 | "if echo " + (if (value) 3 else 0) + " > /proc/sys/net/ipv4/tcp_fastopen; then", 54 | " echo Success.", 55 | "else", 56 | " echo Failed.", 57 | "fi"), null, true) 58 | if (res != null) res.asScala.mkString("\n") else null 59 | } else null 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/scala/com/github/shadowsocks/utils/Typefaces.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.util 24 | 25 | import android.content.Context 26 | import android.graphics.Typeface 27 | import android.util.Log 28 | import com.github.shadowsocks.ShadowsocksApplication.app 29 | 30 | object Typefaces { 31 | def get(c: Context, assetPath: String): Typeface = { 32 | cache synchronized { 33 | if (!cache.containsKey(assetPath)) { 34 | try { 35 | cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath)) 36 | } catch { 37 | case e: Exception => 38 | Log.e("Typefaces", "Could not get typeface '" + assetPath + "' because " + e.getMessage) 39 | app.track(e) 40 | return null 41 | } 42 | } 43 | return cache.get(assetPath) 44 | } 45 | } 46 | 47 | private final val cache = new util.Hashtable[String, Typeface] 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/widget/BoundedCardView.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.content.Context 24 | import android.support.v7.widget.CardView 25 | import android.util.AttributeSet 26 | 27 | /** 28 | * @author Mygod 29 | */ 30 | class BoundedCardView(context: Context, attrs: AttributeSet) extends CardView(context, attrs) with BoundedView { 31 | initAttrs(context, attrs) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/widget/BoundedGridLayout.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.content.Context 24 | import android.support.v7.widget.GridLayout 25 | import android.util.AttributeSet 26 | 27 | /** 28 | * @author Mygod 29 | */ 30 | class BoundedGridLayout(context: Context, attrs: AttributeSet) extends GridLayout(context, attrs) with BoundedView { 31 | initAttrs(context, attrs) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/com/github/shadowsocks/widget/BoundedView.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.content.Context 24 | import android.util.AttributeSet 25 | import android.view.View 26 | import android.view.View.MeasureSpec 27 | import com.github.shadowsocks.R 28 | 29 | /** 30 | * Based on: http://stackoverflow.com/a/6212120/2245107 31 | * 32 | * @author Mygod 33 | */ 34 | trait BoundedView extends View { 35 | private var boundedWidth: Int = _ 36 | private var boundedHeight: Int = _ 37 | 38 | protected def initAttrs(context: Context, attrs: AttributeSet) { 39 | val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedView) 40 | boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0) 41 | boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0) 42 | arr.recycle() 43 | } 44 | 45 | override def onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int): Unit = super.onMeasure( 46 | if (boundedWidth > 0 && boundedWidth < MeasureSpec.getSize(widthMeasureSpec)) 47 | MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthMeasureSpec)) 48 | else widthMeasureSpec, 49 | if (boundedHeight > 0 && boundedHeight < MeasureSpec.getSize(heightMeasureSpec)) 50 | MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightMeasureSpec)) 51 | else heightMeasureSpec) 52 | } 53 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------