Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa8e966e5 | ||
|
|
17e84e58d8 | ||
|
|
10940f2b1d | ||
|
|
dd4fad1a41 | ||
|
|
49a3cfdc34 | ||
|
|
82325ecdba | ||
|
|
555f4f6854 | ||
|
|
e99a0eaafc | ||
|
|
7f2f67f27c | ||
|
|
488d825637 | ||
|
|
f792c8ebfb | ||
|
|
78e57de833 | ||
|
|
017dd425a0 | ||
|
|
04ec29c9ea | ||
|
|
16e39f8b6c | ||
|
|
6dede5c067 | ||
|
|
fb3f7429b7 | ||
|
|
0e0e65c45d | ||
|
|
86da4f535d | ||
|
|
ee48c0773c | ||
|
|
6ade064599 | ||
|
|
8fd8514cc2 | ||
|
|
0aad8f13ed | ||
|
|
14a5324852 | ||
|
|
7c5efdaa82 | ||
|
|
1c37c449d5 | ||
|
|
0f2ae6521b | ||
|
|
3ae15514a5 | ||
|
|
cee74f2563 | ||
|
|
4c1a817c1b | ||
|
|
390d79c9aa | ||
|
|
216cc9efe2 | ||
|
|
3bb746254e | ||
|
|
8fa2e60cf3 | ||
|
|
8657ef87f4 | ||
|
|
da1b404175 | ||
|
|
fcdeb5144a | ||
|
|
270d436017 | ||
|
|
a4e186b759 | ||
|
|
9965a71c93 | ||
|
|
9e09e0329b | ||
|
|
c19eb4e9e5 | ||
|
|
cad6d7b62a | ||
|
|
e4e1a42146 | ||
|
|
76108ec450 | ||
|
|
5b9a0d0659 | ||
|
|
6558f275fe | ||
|
|
c13cf6f5d2 | ||
|
|
cacde5cff1 | ||
|
|
bb2bd82d73 | ||
|
|
0a46eedba1 | ||
|
|
63bc60e513 | ||
|
|
d9eb0e5163 | ||
|
|
fc3d26d626 | ||
|
|
1bb3a6bf5d | ||
|
|
f6cab9b99c | ||
|
|
a24d29196f | ||
|
|
2eb12ee2ae | ||
|
|
ef7701982d |
@@ -2,9 +2,7 @@
|
||||
|
||||
A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
||||
|
||||
Help to translate shadowsocks: http://crowdin.net/project/shadowsocks/invite
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.github.shadowsocks)
|
||||
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
|
||||
|
||||
### CI STATUS
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ platformTarget in Android := "android-23"
|
||||
|
||||
name := "shadowsocks"
|
||||
|
||||
scalaVersion := "2.11.7"
|
||||
scalaVersion := "2.11.8"
|
||||
|
||||
compileOrder in Compile := CompileOrder.JavaThenScala
|
||||
|
||||
@@ -30,14 +30,15 @@ libraryDependencies ++= Seq(
|
||||
"dnsjava" % "dnsjava" % "2.1.7",
|
||||
"com.github.kevinsawicki" % "http-request" % "6.0",
|
||||
"commons-net" % "commons-net" % "3.4",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201602011018",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201602271131",
|
||||
"com.google.zxing" % "android-integration" % "3.2.1",
|
||||
"net.glxn.qrgen" % "android" % "2.0",
|
||||
"com.google.android.gms" % "play-services-base" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-analytics" % "8.4.0",
|
||||
"com.android.support" % "design" % "23.1.1",
|
||||
"com.android.support" % "gridlayout-v7" % "23.1.1",
|
||||
"com.android.support" % "design" % "23.2.1",
|
||||
"com.android.support" % "gridlayout-v7" % "23.2.1",
|
||||
"com.android.support" % "cardview-v7" % "23.2.1",
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
|
||||
"com.j256.ormlite" % "ormlite-core" % "4.48",
|
||||
"com.j256.ormlite" % "ormlite-android" % "4.48",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-snapshots"))(Resolver.ivyStylePatterns)
|
||||
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.17")
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.19")
|
||||
|
||||
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.shadowsocks"
|
||||
android:versionCode="131"
|
||||
android:versionName="2.9.9">
|
||||
android:versionCode="134"
|
||||
android:versionName="2.10.1">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="23"/>
|
||||
@@ -46,6 +49,8 @@
|
||||
<activity
|
||||
android:name=".ParserActivity"
|
||||
android:theme="@style/PopupTheme"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -53,6 +58,11 @@
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="ss"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="ss" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -86,6 +96,17 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ShadowsocksQuickSwitchActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/PopupTheme.QuickSwitch">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.QUICK_SWITCH" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".ShadowsocksRunnerService"
|
||||
android:exported="false">
|
||||
|
||||
@@ -10,6 +10,5 @@ interface IShadowsocksService {
|
||||
oneway void registerCallback(IShadowsocksServiceCallback cb);
|
||||
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
|
||||
|
||||
oneway void start(in Config config);
|
||||
oneway void stop();
|
||||
oneway void use(in Config config);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 221 B |
|
Before Width: | Height: | Size: 127 B |
|
Before Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 430 B |
|
Before Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 88 B |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 343 B |
|
Before Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 483 B |
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 518 B |
|
Before Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 347 B |
|
Before Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 675 B |
|
Before Width: | Height: | Size: 825 B |
|
Before Width: | Height: | Size: 789 B |
|
Before Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -23,11 +23,12 @@
|
||||
android:drawablePadding="20dp"
|
||||
android:maxLines="4"
|
||||
android:scrollHorizontally="false" />
|
||||
<ImageView android:id="@+id/qrcode"
|
||||
<ImageView android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="5dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_social_share"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/qrcode"/>
|
||||
android:contentDescription="@string/share"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
app:cardElevation="5dp"
|
||||
app:cardBackgroundColor="#EEEEEE">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/toolbar_light_dark"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/profilesList"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
</android.support.v7.widget.CardView>
|
||||
@@ -1,8 +1,12 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:showAsAction="never" />
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:showAsAction="never" />
|
||||
android:title="@string/action_import"
|
||||
android:showAsAction="never"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="1"/>
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:showAsAction="never"
|
||||
android:alphabeticShortcut="e"
|
||||
android:numericShortcut="2"/>
|
||||
</menu>
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
android:title="@string/add_profile_methods_manual_settings"
|
||||
android:alphabeticShortcut="m"
|
||||
android:numericShortcut="2"/>
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="3"/>
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:alphabeticShortcut="e"
|
||||
android:numericShortcut="4"/>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<string name="nat_no_root">NAT 模式需要 ROOT 权限</string>
|
||||
<string name="switch_to_vpn">切换到 VPN 模式</string>
|
||||
<string name="stop">停止服务</string>
|
||||
<string name="quick_switch">快速切换</string>
|
||||
<string name="stopping">正在关闭……</string>
|
||||
<string name="vpn_error">后台服务启动失败:%s</string>
|
||||
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
|
||||
@@ -59,7 +60,11 @@
|
||||
<string name="flush_dnscache_summary_root">只需在 NAT 模式下使用,需要 ROOT 权限</string>
|
||||
<string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string>
|
||||
<string name="flushing">刷新中……</string>
|
||||
<string name="qrcode">二维码</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="share_message">扫描二维码或者使用 NFC(Android Beam)</string>
|
||||
<string name="share_message_without_nfc">扫描二维码</string>
|
||||
<string name="share_message_nfc_disabled">扫描二维码,NFC(Android Beam)被禁用</string>
|
||||
<string name="turn_on_nfc">打开 NFC & Android Beam</string>
|
||||
<string name="add_profile">添加配置文件</string>
|
||||
|
||||
<string name="nat">NAT 模式 (仅限调试)</string>
|
||||
@@ -89,6 +94,7 @@
|
||||
<string name="profile_manager_dialog_content">• 点击选择配置文件;\n• 滑动删除;\n•
|
||||
长按拖动改变顺序。</string>
|
||||
<string name="gotcha">好的</string>
|
||||
<string name="copy_url">复制 URL</string>
|
||||
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
|
||||
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
|
||||
<string name="add_profile_methods_manual_settings">手动设置</string>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<string name="stopping">Shutting down…</string>
|
||||
<string name="vpn_error">%s</string>
|
||||
<string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string>
|
||||
<string name="quick_switch">Quick Switch</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">Close</string>
|
||||
@@ -90,7 +91,11 @@
|
||||
<string name="recovery_summary">Reset all the background services</string>
|
||||
<string name="about">About</string>
|
||||
<string name="about_title">Shadowsocks %s</string>
|
||||
<string name="qrcode">QR Code</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="share_message">Scan QRCode or Use NFC (Android Beam)</string>
|
||||
<string name="share_message_without_nfc">Scan QRCode</string>
|
||||
<string name="share_message_nfc_disabled">Scan QRCode, NFC (Android Beam) is disabled</string>
|
||||
<string name="turn_on_nfc">Turn on NFC & Android Beam</string>
|
||||
<string name="flush_dnscache">Flush DNS cache</string>
|
||||
<string name="flush_dnscache_summary">Only useful in NAT mode</string>
|
||||
<string name="flush_dnscache_summary_root">Only useful in NAT mode. Requires ROOT permission.</string>
|
||||
@@ -112,6 +117,7 @@
|
||||
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
|
||||
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
|
||||
<string name="add_profile_methods_manual_settings">Manual Settings</string>
|
||||
<string name="copy_url">Copy URL</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">Removed</item>
|
||||
<item quantity="other">%d items removed</item>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<style name="Toolbar.Logo" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">24sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:alertDialogTheme">@style/Theme.Material.Dialog.Alert</item>
|
||||
<item name="android:dialogTheme">@style/Theme.Material.Dialog</item>
|
||||
@@ -14,19 +15,23 @@
|
||||
<item name="colorPrimary">@color/material_accent_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_accent_700</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material.DialogBase" parent="Theme.AppCompat.Light.Dialog">
|
||||
<item name="colorAccent">@color/material_accent_500</item>
|
||||
<item name="colorPrimary">@color/material_accent_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_accent_700</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material.Dialog.AlertBase" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||
<item name="colorAccent">@color/material_accent_500</item>
|
||||
<item name="colorPrimary">@color/material_accent_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_accent_700</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material.Dialog" parent="Theme.Material.DialogBase">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material.Dialog.Alert" parent="Theme.Material.Dialog.AlertBase">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
@@ -39,4 +44,10 @@
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="PopupTheme.QuickSwitch" parent="PopupTheme">
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -154,26 +154,22 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
return true
|
||||
case R.id.action_import =>
|
||||
if (clipboard.hasPrimaryClip) {
|
||||
val clipdata = clipboard.getPrimaryClip
|
||||
val label = clipdata.getDescription.getLabel
|
||||
if (label == Key.proxied) {
|
||||
val proxiedAppSequence = clipdata.getItemAt(0).getText
|
||||
if (proxiedAppSequence != null) {
|
||||
val proxiedAppString = proxiedAppSequence.toString
|
||||
if (!proxiedAppString.isEmpty) {
|
||||
val editor = prefs.edit
|
||||
val i = proxiedAppString.indexOf('\n')
|
||||
if (i < 0)
|
||||
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
|
||||
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
|
||||
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
// Restart activity
|
||||
appListView.setVisibility(View.GONE)
|
||||
loadingView.setVisibility(View.VISIBLE)
|
||||
reloadApps()
|
||||
return true
|
||||
}
|
||||
val proxiedAppSequence = clipboard.getPrimaryClip.getItemAt(0).getText
|
||||
if (proxiedAppSequence != null) {
|
||||
val proxiedAppString = proxiedAppSequence.toString
|
||||
if (!proxiedAppString.isEmpty) {
|
||||
val editor = prefs.edit
|
||||
val i = proxiedAppString.indexOf('\n')
|
||||
if (i < 0)
|
||||
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
|
||||
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
|
||||
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
// Restart activity
|
||||
appListView.setVisibility(View.GONE)
|
||||
loadingView.setVisibility(View.VISIBLE)
|
||||
reloadApps()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,12 @@ package com.github.shadowsocks
|
||||
import java.util.{Timer, TimerTask}
|
||||
|
||||
import android.app.Service
|
||||
import android.content.{Intent, Context}
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.{Handler, RemoteCallbackList}
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
trait BaseService extends Service {
|
||||
|
||||
@@ -57,6 +59,13 @@ trait BaseService extends Service {
|
||||
|
||||
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
|
||||
var callbacksCount: Int = _
|
||||
lazy val handler = new Handler(getContext.getMainLooper)
|
||||
|
||||
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => {
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
stopRunner(true)
|
||||
}
|
||||
var closeReceiverRegistered: Boolean = _
|
||||
|
||||
val binder = new IShadowsocksService.Stub {
|
||||
override def getMode: Int = {
|
||||
@@ -94,17 +103,15 @@ trait BaseService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
override def stop() {
|
||||
if (state == State.CONNECTED) {
|
||||
stopRunner()
|
||||
}
|
||||
}
|
||||
|
||||
override def start(config: Config) {
|
||||
if (state == State.STOPPED) {
|
||||
startRunner(config)
|
||||
}
|
||||
}
|
||||
override def use(config: Config) = synchronized(state match {
|
||||
case State.STOPPED => if (config != null) startRunner(config)
|
||||
case State.CONNECTED =>
|
||||
if (config == null) stopRunner(true) else if (config.profileId != BaseService.this.config.profileId) {
|
||||
stopRunner(false)
|
||||
startRunner(config)
|
||||
}
|
||||
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
|
||||
})
|
||||
}
|
||||
|
||||
def startRunner(config: Config) {
|
||||
@@ -114,9 +121,24 @@ trait BaseService extends Service {
|
||||
TrafficMonitor.reset()
|
||||
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
|
||||
trafficMonitorThread.start()
|
||||
|
||||
if (!closeReceiverRegistered) {
|
||||
// register close receiver
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
registerReceiver(closeReceiver, filter)
|
||||
closeReceiverRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
def stopRunner() {
|
||||
def stopRunner(stopService: Boolean) {
|
||||
// clean up recevier
|
||||
if (closeReceiverRegistered) {
|
||||
unregisterReceiver(closeReceiver)
|
||||
closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
// Make sure update total traffic when stopping the runner
|
||||
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
|
||||
@@ -130,7 +152,7 @@ trait BaseService extends Service {
|
||||
changeState(State.STOPPED)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
stopSelf()
|
||||
if (stopService) stopSelf()
|
||||
}
|
||||
|
||||
def updateTrafficTotal(tx: Long, rx: Long) {
|
||||
@@ -158,7 +180,6 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
def updateTrafficRate() {
|
||||
val handler = new Handler(getContext.getMainLooper)
|
||||
handler.post(() => {
|
||||
if (callbacksCount > 0) {
|
||||
val txRate = TrafficMonitor.txRate
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2016 <max.c.lv@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.io.{IOException, InputStream, OutputStream}
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.util.concurrent.Semaphore
|
||||
|
||||
import android.util.Log
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* @author ayanamist@gmail.com
|
||||
*/
|
||||
class GuardedProcess(cmd: Seq[String]) extends Process {
|
||||
private val TAG = classOf[GuardedProcess].getSimpleName
|
||||
|
||||
@volatile private var guardThread: Thread = _
|
||||
@volatile private var isDestroyed: Boolean = _
|
||||
@volatile private var process: Process = _
|
||||
|
||||
def start(onRestartCallback: () => Unit = null): GuardedProcess = {
|
||||
val semaphore = new Semaphore(1)
|
||||
semaphore.acquire
|
||||
@volatile var ioException: IOException = null
|
||||
|
||||
guardThread = new Thread(() => {
|
||||
try {
|
||||
var callback: () => Unit = null
|
||||
while (!isDestroyed) {
|
||||
Log.i(TAG, "start process: " + cmd)
|
||||
val startTime = currentTimeMillis
|
||||
|
||||
process = new ProcessBuilder(cmd).redirectErrorStream(true).start
|
||||
|
||||
if (callback == null) callback = onRestartCallback else callback()
|
||||
|
||||
semaphore.release
|
||||
process.waitFor
|
||||
|
||||
if (currentTimeMillis - startTime < 1000) {
|
||||
Log.w(TAG, "process exit too fast, stop guard: " + cmd)
|
||||
isDestroyed = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case ignored: InterruptedException =>
|
||||
Log.i(TAG, "thread interrupt, destroy process: " + cmd)
|
||||
process.destroy()
|
||||
case e: IOException => ioException = e
|
||||
} finally semaphore.release
|
||||
}, "GuardThread-" + cmd)
|
||||
|
||||
guardThread.start()
|
||||
semaphore.acquire
|
||||
|
||||
if (ioException != null) {
|
||||
throw ioException
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
def destroy() {
|
||||
isDestroyed = true
|
||||
guardThread.interrupt()
|
||||
process.destroy()
|
||||
try guardThread.join() catch {
|
||||
case ignored: InterruptedException =>
|
||||
}
|
||||
}
|
||||
|
||||
def exitValue: Int = throw new UnsupportedOperationException
|
||||
def getErrorStream: InputStream = throw new UnsupportedOperationException
|
||||
def getInputStream: InputStream = throw new UnsupportedOperationException
|
||||
def getOutputStream: OutputStream = throw new UnsupportedOperationException
|
||||
|
||||
@throws(classOf[InterruptedException])
|
||||
def waitFor = {
|
||||
guardThread.join()
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -39,20 +39,43 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.{DialogInterface, Intent}
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.nfc.{NdefMessage, NfcAdapter}
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.{AppCompatActivity, AlertDialog}
|
||||
import android.support.v7.app.{AlertDialog, AppCompatActivity}
|
||||
import android.text.TextUtils
|
||||
import android.view.WindowManager
|
||||
import com.github.shadowsocks.utils.Parser
|
||||
|
||||
class ParserActivity extends AppCompatActivity {
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val data = getIntent.getData.toString
|
||||
val profile = Parser.parse(data)
|
||||
if (profile.isEmpty) { // ignore
|
||||
handledSharedIntent()
|
||||
}
|
||||
|
||||
override def onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handledSharedIntent()
|
||||
}
|
||||
|
||||
def handledSharedIntent() {
|
||||
val intent = getIntent()
|
||||
val sharedStr = intent.getAction match {
|
||||
case Intent.ACTION_VIEW => intent.getData.toString
|
||||
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
|
||||
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||
if (rawMsgs != null && rawMsgs.nonEmpty)
|
||||
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
|
||||
else null
|
||||
case _ => null
|
||||
}
|
||||
if (TextUtils.isEmpty(sharedStr)) return
|
||||
val profiles = Parser.findAll(sharedStr).toList
|
||||
if (profiles.isEmpty) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@@ -60,13 +83,12 @@ class ParserActivity extends AppCompatActivity {
|
||||
val dialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_profile_dialog)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.yes, ((dialog: DialogInterface, id: Int) => {
|
||||
ShadowsocksApplication.profileManager.createProfile(profile.get)
|
||||
}): DialogInterface.OnClickListener)
|
||||
.setPositiveButton(android.R.string.yes, ((_, _) =>
|
||||
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)): DialogInterface.OnClickListener)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.setMessage(data)
|
||||
.setMessage(profiles.mkString("\n"))
|
||||
.create()
|
||||
dialog.setOnDismissListener((dialog: DialogInterface) => finish()) // builder method was added in API 17
|
||||
dialog.setOnDismissListener((_: DialogInterface) => finish()) // builder method was added in API 17
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.{Bundle, Handler}
|
||||
import java.nio.charset.Charset
|
||||
|
||||
import android.content._
|
||||
import android.nfc.NfcAdapter.CreateNdefMessageCallback
|
||||
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
|
||||
import android.os.{Build, Bundle, Handler}
|
||||
import android.provider.Settings
|
||||
import android.support.v7.app.{AlertDialog, AppCompatActivity}
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
@@ -9,52 +14,70 @@ import android.support.v7.widget.helper.ItemTouchHelper
|
||||
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.text.style.TextAppearanceSpan
|
||||
import android.text.{SpannableStringBuilder, Spanned}
|
||||
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
|
||||
import android.view.{LayoutInflater, MenuItem, View, ViewGroup}
|
||||
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils.{Parser, TrafficMonitor, Utils}
|
||||
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
|
||||
import com.github.shadowsocks.widget.UndoSnackbarManager
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object ProfileManagerActivity {
|
||||
private final val profileTip = "profileTip"
|
||||
}
|
||||
|
||||
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext {
|
||||
import ProfileManagerActivity._
|
||||
|
||||
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
|
||||
with CreateNdefMessageCallback {
|
||||
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
var item: Profile = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
{
|
||||
val qrcode = itemView.findViewById(R.id.qrcode)
|
||||
qrcode.setOnClickListener((v: View) => {
|
||||
val shareBtn = itemView.findViewById(R.id.share)
|
||||
shareBtn.setOnClickListener(_ => {
|
||||
val url = item.toString
|
||||
if (nfcBeamEnable) {
|
||||
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this)
|
||||
nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
|
||||
}
|
||||
val image = new ImageView(ProfileManagerActivity.this)
|
||||
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
|
||||
val qrcode = QRCode.from(Parser.generate(item))
|
||||
val qrcode = QRCode.from(url)
|
||||
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
|
||||
.asInstanceOf[QRCode].bitmap()
|
||||
image.setImageBitmap(qrcode)
|
||||
|
||||
new AlertDialog.Builder(ProfileManagerActivity.this)
|
||||
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setPositiveButton(R.string.close, null)
|
||||
.setNegativeButton(R.string.copy_url, ((_, _) =>
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
|
||||
.setView(image)
|
||||
.setTitle(R.string.share)
|
||||
.create()
|
||||
.show()
|
||||
if (!nfcAvailable){
|
||||
dialog.setMessage(getString(R.string.share_message_without_nfc))
|
||||
} else {
|
||||
if (!nfcBeamEnable) {
|
||||
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
|
||||
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
|
||||
} else {
|
||||
startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||
}
|
||||
): DialogInterface.OnClickListener)
|
||||
} else {
|
||||
dialog.setMessage(getString(R.string.share_message))
|
||||
dialog.setOnDismissListener(_ =>
|
||||
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
})
|
||||
qrcode.setOnLongClickListener((v: View) => {
|
||||
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.qrcode, Toast.LENGTH_SHORT), qrcode,
|
||||
shareBtn.setOnLongClickListener(_ => {
|
||||
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.share, Toast.LENGTH_SHORT), shareBtn,
|
||||
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show
|
||||
true
|
||||
})
|
||||
@@ -150,6 +173,14 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
private lazy val profilesAdapter = new ProfilesAdapter
|
||||
private var undoManager: UndoSnackbarManager[Profile] = _
|
||||
|
||||
private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
|
||||
|
||||
private var nfcAdapter : NfcAdapter = _
|
||||
private var nfcShareItem: Array[Byte] = _
|
||||
private var nfcAvailable = false
|
||||
private var nfcEnable = false
|
||||
private var nfcBeamEnable = false
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.layout_profiles)
|
||||
@@ -193,13 +224,38 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
|
||||
})
|
||||
|
||||
if (ShadowsocksApplication.settings.getBoolean(profileTip, true)) {
|
||||
ShadowsocksApplication.settings.edit.putBoolean(profileTip, false).commit
|
||||
if (ShadowsocksApplication.settings.getBoolean(Key.profileTip, true)) {
|
||||
ShadowsocksApplication.settings.edit.putBoolean(Key.profileTip, false).commit
|
||||
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
|
||||
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create.show
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override def onResume() {
|
||||
super.onResume()
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (nfcAdapter != null){
|
||||
nfcAvailable = true
|
||||
if (nfcAdapter.isEnabled) {
|
||||
nfcEnable = true
|
||||
if (nfcAdapter.isNdefPushEnabled) {
|
||||
nfcBeamEnable = true
|
||||
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
|
||||
} else {
|
||||
nfcBeamEnable = false
|
||||
}
|
||||
} else{
|
||||
nfcEnable = false
|
||||
nfcBeamEnable = false
|
||||
}
|
||||
} else {
|
||||
nfcAvailable = false
|
||||
nfcEnable = false
|
||||
nfcBeamEnable = false
|
||||
}
|
||||
}
|
||||
|
||||
override def onStart() {
|
||||
super.onStart()
|
||||
registerCallback
|
||||
@@ -218,13 +274,17 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (scanResult != null) Parser.parse(scanResult.getContents) match {
|
||||
case Some(profile) => ShadowsocksApplication.profileManager.createProfile(profile)
|
||||
case _ => // ignore
|
||||
if (scanResult != null) {
|
||||
val contents = scanResult.getContents
|
||||
if (!TextUtils.isEmpty(contents))
|
||||
Parser.findAll(contents).foreach(ShadowsocksApplication.profileManager.createProfile)
|
||||
}
|
||||
}
|
||||
|
||||
def onMenuItemClick(item: MenuItem) = item.getItemId match {
|
||||
def createNdefMessage(nfcEvent: NfcEvent) =
|
||||
new NdefMessage(Array(new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem)))
|
||||
|
||||
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
|
||||
case R.id.scan_qr_code =>
|
||||
val integrator = new IntentIntegrator(this)
|
||||
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
|
||||
@@ -237,6 +297,25 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
ShadowsocksApplication.switchProfile(ShadowsocksApplication.profileManager.save.id)
|
||||
finish
|
||||
true
|
||||
case R.id.action_import =>
|
||||
if (clipboard.hasPrimaryClip) {
|
||||
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
|
||||
if (profiles.nonEmpty) {
|
||||
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show
|
||||
return true
|
||||
}
|
||||
}
|
||||
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show
|
||||
true
|
||||
case R.id.action_export =>
|
||||
ShadowsocksApplication.profileManager.getAllProfiles match {
|
||||
case Some(profiles) =>
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
|
||||
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show
|
||||
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show
|
||||
}
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
||||
import android.os.{RemoteException, IBinder}
|
||||
import com.github.shadowsocks.aidl.{IShadowsocksServiceCallback, IShadowsocksService}
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
trait ServiceBoundContext extends Context {
|
||||
|
||||
class ShadowsocksServiceConnection extends ServiceConnection {
|
||||
override def onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
service.linkToDeath(new ShadowsocksDeathRecipient(ServiceBoundContext.this), 0)
|
||||
bgService = IShadowsocksService.Stub.asInterface(service)
|
||||
registerCallback
|
||||
ServiceBoundContext.this.onServiceConnected()
|
||||
@@ -27,17 +26,18 @@ trait ServiceBoundContext extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
def registerCallback = if (bgService != null && callback != null && !callbackRegistered) try {
|
||||
protected def registerCallback = if (bgService != null && callback != null && !callbackRegistered) try {
|
||||
bgService.registerCallback(callback)
|
||||
callbackRegistered = true
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
def unregisterCallback = if (bgService != null && callback != null && callbackRegistered) try {
|
||||
bgService.unregisterCallback(callback)
|
||||
|
||||
protected def unregisterCallback = {
|
||||
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
|
||||
case ignored: RemoteException =>
|
||||
}
|
||||
callbackRegistered = false
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
|
||||
def onServiceConnected() = ()
|
||||
@@ -66,16 +66,12 @@ trait ServiceBoundContext extends Context {
|
||||
}
|
||||
|
||||
def deattachService() {
|
||||
if (bgService != null) {
|
||||
if (callback != null) {
|
||||
unregisterCallback
|
||||
callback = null
|
||||
}
|
||||
if (connection != null) {
|
||||
unbindService(connection)
|
||||
connection = null
|
||||
}
|
||||
bgService = null
|
||||
unregisterCallback
|
||||
callback = null
|
||||
if (connection != null) {
|
||||
unbindService(connection)
|
||||
connection = null
|
||||
}
|
||||
bgService = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ class Shadowsocks
|
||||
}
|
||||
}
|
||||
|
||||
private def crashRecovery() {
|
||||
def crashRecovery() {
|
||||
val cmd = new ArrayBuffer[String]()
|
||||
|
||||
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
|
||||
@@ -318,8 +318,6 @@ class Shadowsocks
|
||||
changeSwitch(checked = false)
|
||||
}
|
||||
|
||||
def isReady: Boolean = checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null
|
||||
|
||||
def prepareStartService() {
|
||||
ThrowableFuture {
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
@@ -330,7 +328,7 @@ class Shadowsocks
|
||||
handler.post(() => onActivityResult(Shadowsocks.REQUEST_CONNECT, Activity.RESULT_OK, null))
|
||||
}
|
||||
} else {
|
||||
serviceStart()
|
||||
serviceLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,18 +356,9 @@ class Shadowsocks
|
||||
|
||||
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
|
||||
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
|
||||
fab.setOnClickListener((v: View) => {
|
||||
serviceStarted = !serviceStarted
|
||||
serviceStarted match {
|
||||
case true =>
|
||||
if (isReady)
|
||||
prepareStartService()
|
||||
else
|
||||
changeSwitch(checked = false)
|
||||
case false =>
|
||||
serviceStop()
|
||||
}
|
||||
})
|
||||
fab.setOnClickListener((v: View) => if (serviceStarted) serviceStop()
|
||||
else if (checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null) prepareStartService()
|
||||
else changeSwitch(checked = false))
|
||||
fab.setOnLongClickListener((v: View) => {
|
||||
Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect,
|
||||
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show
|
||||
@@ -377,25 +366,9 @@ class Shadowsocks
|
||||
})
|
||||
updateTraffic(0, 0, 0, 0)
|
||||
|
||||
// Bind to the service
|
||||
handler.post(() => attachService)
|
||||
}
|
||||
|
||||
def reloadProfile() {
|
||||
currentProfile = ShadowsocksApplication.currentProfile match {
|
||||
case Some(profile) => profile // updated
|
||||
case None => // removed
|
||||
ShadowsocksApplication.profileManager.getFirstProfile match {
|
||||
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
|
||||
case None => ShadowsocksApplication.profileManager.createDefault()
|
||||
}
|
||||
}
|
||||
|
||||
updatePreferenceScreen()
|
||||
|
||||
serviceStop()
|
||||
}
|
||||
|
||||
protected override def onPause() {
|
||||
super.onPause()
|
||||
ShadowsocksApplication.profileManager.save
|
||||
@@ -446,7 +419,20 @@ class Shadowsocks
|
||||
ConfigUtils.refresh(this)
|
||||
|
||||
// Check if current profile changed
|
||||
if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile()
|
||||
if (ShadowsocksApplication.profileId != currentProfile.id) {
|
||||
currentProfile = ShadowsocksApplication.currentProfile match {
|
||||
case Some(profile) => profile // updated
|
||||
case None => // removed
|
||||
ShadowsocksApplication.profileManager.getFirstProfile match {
|
||||
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
|
||||
case None => ShadowsocksApplication.profileManager.createDefault()
|
||||
}
|
||||
}
|
||||
|
||||
updatePreferenceScreen()
|
||||
|
||||
if (serviceStarted) serviceLoad()
|
||||
}
|
||||
|
||||
updateState()
|
||||
}
|
||||
@@ -457,7 +443,7 @@ class Shadowsocks
|
||||
adView = new AdView(this)
|
||||
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
|
||||
adView.setAdSize(AdSize.SMART_BANNER)
|
||||
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 0)
|
||||
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1)
|
||||
adView.loadAd(new AdRequest.Builder().build())
|
||||
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
|
||||
|
||||
@@ -484,7 +470,9 @@ class Shadowsocks
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
def install() {
|
||||
def reset() {
|
||||
crashRecovery()
|
||||
|
||||
copyAssets(System.getABI)
|
||||
|
||||
val ab = new ArrayBuffer[String]
|
||||
@@ -494,14 +482,8 @@ class Shadowsocks
|
||||
Console.runCommand(ab.toArray)
|
||||
}
|
||||
|
||||
def reset() {
|
||||
crashRecovery()
|
||||
|
||||
install()
|
||||
}
|
||||
|
||||
def recovery() {
|
||||
serviceStop()
|
||||
if (serviceStarted) serviceStop()
|
||||
val h = showProgress(R.string.recovering)
|
||||
ThrowableFuture {
|
||||
reset()
|
||||
@@ -520,14 +502,14 @@ class Shadowsocks
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
|
||||
case Activity.RESULT_OK =>
|
||||
serviceStart()
|
||||
serviceLoad()
|
||||
case _ =>
|
||||
cancelStart()
|
||||
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
|
||||
}
|
||||
|
||||
def serviceStop() {
|
||||
if (bgService != null) bgService.stop()
|
||||
if (bgService != null) bgService.use(null)
|
||||
}
|
||||
|
||||
def checkText(key: String): Boolean = {
|
||||
@@ -538,8 +520,8 @@ class Shadowsocks
|
||||
}
|
||||
|
||||
/** Called when connect button is clicked. */
|
||||
def serviceStart() {
|
||||
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
def serviceLoad() {
|
||||
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
changeSwitch(checked = false)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* @author chentaov5@gmail.com
|
||||
*/
|
||||
class ShadowsocksDeathRecipient(val mContext: ServiceBoundContext)
|
||||
extends IBinder.DeathRecipient {
|
||||
|
||||
val TAG = "ShadowsocksDeathRecipient"
|
||||
|
||||
override def binderDied(): Unit = {
|
||||
Log.d(TAG, "[ShadowsocksDeathRecipient] binder died.")
|
||||
mContext match {
|
||||
case ss: Shadowsocks =>
|
||||
ss.deattachService
|
||||
ss.crashRecovery
|
||||
ss.attachService
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ import android.content.pm.{PackageInfo, PackageManager}
|
||||
import android.net.{ConnectivityManager, Network}
|
||||
import android.os._
|
||||
import android.util.{Log, SparseArray}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.aidl.Config
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
@@ -65,7 +64,6 @@ class ShadowsocksNatService extends BaseService {
|
||||
"-j DNAT --to-destination 127.0.0.1:8123"
|
||||
|
||||
private var notification: ShadowsocksNotification = _
|
||||
var closeReceiver: BroadcastReceiver = _
|
||||
var connReceiver: BroadcastReceiver = _
|
||||
val myUid = android.os.Process.myUid()
|
||||
|
||||
@@ -196,10 +194,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
sslocalProcess = new ProcessBuilder()
|
||||
.command(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
sslocalProcess = new GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
def startTunnel() {
|
||||
@@ -225,10 +220,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
|
||||
sstunnelProcess = new ProcessBuilder()
|
||||
.command(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
sstunnelProcess = new GuardedProcess(cmd).start()
|
||||
|
||||
} else {
|
||||
val conf = ConfigUtils
|
||||
@@ -251,16 +243,13 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmdBuf.mkString(" "))
|
||||
|
||||
sstunnelProcess = new ProcessBuilder()
|
||||
.command(cmdBuf)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
sstunnelProcess = new GuardedProcess(cmdBuf).start()
|
||||
}
|
||||
}
|
||||
|
||||
def startDnsDaemon() {
|
||||
|
||||
val conf = if (config.route == Route.BYPASS_CHN) {
|
||||
val conf = if (config.route == Route.BYPASS_CHN || config.route == Route.BYPASS_LAN_CHN) {
|
||||
val reject = ConfigUtils.getRejectList(getContext)
|
||||
val blackList = ConfigUtils.getBlackList(getContext)
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
@@ -277,10 +266,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
|
||||
pdnsdProcess = new ProcessBuilder()
|
||||
.command(cmd.split(" ").toSeq)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
|
||||
}
|
||||
|
||||
def getVersionName: String = {
|
||||
@@ -304,10 +290,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
})
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
redsocksProcess = new ProcessBuilder()
|
||||
.command(cmd.split(" ").toSeq)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
redsocksProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@@ -401,16 +384,6 @@ class ShadowsocksNatService extends BaseService {
|
||||
}
|
||||
super.startRunner(config)
|
||||
|
||||
// register close receiver
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
closeReceiver = (context: Context, intent: Intent) => {
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
stopRunner()
|
||||
}
|
||||
registerReceiver(closeReceiver, filter)
|
||||
|
||||
ShadowsocksApplication.track(TAG, "start")
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
@@ -423,7 +396,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
this.config = null
|
||||
}
|
||||
}
|
||||
@@ -454,23 +427,17 @@ class ShadowsocksNatService extends BaseService {
|
||||
notification = new ShadowsocksNotification(this, config.profileName, true)
|
||||
} else {
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def stopRunner() {
|
||||
override def stopRunner(stopService: Boolean) {
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPING)
|
||||
|
||||
// clean up recevier
|
||||
if (closeReceiver != null) {
|
||||
unregisterReceiver(closeReceiver)
|
||||
closeReceiver = null
|
||||
}
|
||||
|
||||
if (notification != null) notification.destroy()
|
||||
|
||||
ShadowsocksApplication.track(TAG, "stop")
|
||||
@@ -478,7 +445,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
// reset NAT
|
||||
killProcesses()
|
||||
|
||||
super.stopRunner()
|
||||
super.stopRunner(stopService)
|
||||
}
|
||||
|
||||
override def getTag = TAG
|
||||
|
||||
@@ -41,6 +41,12 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, service.getString(R.string.stop),
|
||||
PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
|
||||
ShadowsocksApplication.profileManager.getAllProfiles match {
|
||||
case Some(profiles) => if (profiles.length > 1)
|
||||
builder.addAction(android.R.drawable.ic_menu_manage, service.getString(R.string.quick_switch),
|
||||
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
|
||||
case _ =>
|
||||
}
|
||||
private lazy val style = new BigTextStyle(builder)
|
||||
private val showOnUnlock = visible && Utils.isLollipopOrAbove
|
||||
private var isVisible = true
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.{Build, Bundle}
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import android.widget.CheckedTextView
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
/**
|
||||
* Created by Lucas on 3/10/16.
|
||||
*/
|
||||
class ShadowsocksQuickSwitchActivity extends AppCompatActivity {
|
||||
|
||||
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
{
|
||||
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
|
||||
view.setBackgroundResource(typedArray.getResourceId(0, 0))
|
||||
typedArray.recycle
|
||||
}
|
||||
private var item: Profile = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
def bind(item: Profile) {
|
||||
this.item = item
|
||||
text.setText(item.name)
|
||||
text.setChecked(item.id == ShadowsocksApplication.profileId)
|
||||
}
|
||||
|
||||
def onClick(v: View) {
|
||||
ShadowsocksApplication.switchProfile(item.id)
|
||||
Utils.startSsService(ShadowsocksQuickSwitchActivity.this)
|
||||
finish
|
||||
}
|
||||
}
|
||||
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
|
||||
def getItemCount = profiles.length
|
||||
|
||||
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = i match {
|
||||
case _ => vh.bind(profiles(i))
|
||||
}
|
||||
|
||||
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
|
||||
|
||||
def onCreateViewHolder(vg: ViewGroup, i: Int) = new ProfileViewHolder(LayoutInflater.from(vg.getContext)
|
||||
.inflate(Resources.getSystem.getIdentifier(name, "layout", "android"), vg, false))
|
||||
}
|
||||
|
||||
private val profilesAdapter = new ProfilesAdapter
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.layout_quick_switch)
|
||||
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(R.string.quick_switch)
|
||||
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
val lm = new LinearLayoutManager(this)
|
||||
profilesList.setLayoutManager(lm)
|
||||
profilesList.setItemAnimator(new DefaultItemAnimator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
if (ShadowsocksApplication.profileId >= 0) lm.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
||||
case (profile, i) if profile.id == ShadowsocksApplication.profileId => i + 1
|
||||
}.getOrElse(0))
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
onActivityResult(Shadowsocks.REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||
}
|
||||
} else {
|
||||
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
resultCode match {
|
||||
case Activity.RESULT_OK =>
|
||||
if (bgService != null) {
|
||||
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
}
|
||||
case _ =>
|
||||
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
|
||||
|
||||
@@ -61,11 +61,11 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
|
||||
val intent = VpnService.prepare(ShadowsocksRunnerService.this)
|
||||
if (intent == null) {
|
||||
if (bgService != null) {
|
||||
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
@@ -196,12 +196,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
private var enabled = true
|
||||
def setEnabled(enabled: Boolean) {
|
||||
this.enabled = enabled
|
||||
findPreference(Key.isNAT).setEnabled(enabled)
|
||||
for (name <- PROXY_PREFS) {
|
||||
val pref = findPreference(name)
|
||||
if (pref != null) pref.setEnabled(enabled)
|
||||
}
|
||||
for (name <- FEATURE_PREFS) {
|
||||
for (name <- Key.isNAT #:: PROXY_PREFS.toStream #::: FEATURE_PREFS.toStream) {
|
||||
val pref = findPreference(name)
|
||||
if (pref != null) pref.setEnabled(enabled &&
|
||||
(name != Key.isProxyApps || Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
|
||||
|
||||
@@ -44,18 +44,15 @@ import java.lang.Process
|
||||
import java.util.Locale
|
||||
|
||||
import android.content._
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.content.pm.{PackageInfo, PackageManager}
|
||||
import android.net.VpnService
|
||||
import android.os._
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
|
||||
import com.github.shadowsocks.aidl.Config
|
||||
import com.github.shadowsocks.utils._
|
||||
import org.apache.commons.net.util.SubnetUtils
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
@@ -66,7 +63,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
var conn: ParcelFileDescriptor = _
|
||||
var vpnThread: ShadowsocksVpnThread = _
|
||||
private var notification: ShadowsocksNotification = _
|
||||
var closeReceiver: BroadcastReceiver = _
|
||||
|
||||
var sslocalProcess: Process = _
|
||||
var sstunnelProcess: Process = _
|
||||
@@ -110,10 +106,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
override def onRevoke() {
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
}
|
||||
|
||||
override def stopRunner() {
|
||||
override def stopRunner(stopService: Boolean) {
|
||||
|
||||
if (vpnThread != null) {
|
||||
vpnThread.stopThread()
|
||||
@@ -136,13 +132,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
conn = null
|
||||
}
|
||||
|
||||
// clean up recevier
|
||||
if (closeReceiver != null) {
|
||||
unregisterReceiver(closeReceiver)
|
||||
closeReceiver = null
|
||||
}
|
||||
|
||||
super.stopRunner()
|
||||
super.stopRunner(stopService)
|
||||
}
|
||||
|
||||
def getVersionName: String = {
|
||||
@@ -183,16 +173,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
vpnThread = new ShadowsocksVpnThread(this)
|
||||
vpnThread.start()
|
||||
|
||||
// register close receiver
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
closeReceiver = (context: Context, intent: Intent) => {
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
stopRunner()
|
||||
}
|
||||
registerReceiver(closeReceiver, filter)
|
||||
|
||||
// ensure the VPNService is prepared
|
||||
if (VpnService.prepare(this) != null) {
|
||||
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
|
||||
@@ -213,7 +193,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
this.config = null
|
||||
}
|
||||
}
|
||||
@@ -241,7 +221,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
notification = new ShadowsocksNotification(this, config.profileName)
|
||||
} else {
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,18 +236,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
val fd = startVpn()
|
||||
|
||||
if (fd != -1) {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000 * tries)
|
||||
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
|
||||
return true
|
||||
}
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
false
|
||||
sendFd(fd)
|
||||
}
|
||||
|
||||
def startShadowsocksDaemon() {
|
||||
@@ -307,10 +276,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
|
||||
sslocalProcess = new ProcessBuilder()
|
||||
.command(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
sslocalProcess = new GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
def startDnsTunnel() = {
|
||||
@@ -335,16 +301,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
|
||||
sstunnelProcess = new ProcessBuilder()
|
||||
.command(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
sstunnelProcess = new GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
def startDnsDaemon() {
|
||||
val ipv6 = if (config.isIpv6) "" else "reject = ::/0;"
|
||||
val conf = {
|
||||
if (config.route == Route.BYPASS_CHN) {
|
||||
if (config.route == Route.BYPASS_CHN || config.route == Route.BYPASS_LAN_CHN) {
|
||||
val reject = ConfigUtils.getRejectList(getContext)
|
||||
val blackList = ConfigUtils.getBlackList(getContext)
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
@@ -361,10 +324,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
|
||||
pdnsdProcess = new ProcessBuilder()
|
||||
.command(cmd.split(" ").toSeq)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
|
||||
}
|
||||
|
||||
override def getContext = getBaseContext
|
||||
@@ -401,7 +361,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.route == Route.ALL) {
|
||||
if (config.route == Route.ALL || config.route == Route.BYPASS_CHN) {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
} else {
|
||||
val privateList = getResources.getStringArray(R.array.bypass_private_route)
|
||||
@@ -426,7 +386,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
if (conn == null) {
|
||||
stopRunner()
|
||||
stopRunner(true)
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -454,14 +414,25 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
|
||||
tun2socksProcess = new ProcessBuilder()
|
||||
.command(cmd.split(" ").toSeq)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
tun2socksProcess = new GuardedProcess(cmd.split(" ").toSeq).start(() => sendFd(fd))
|
||||
|
||||
fd
|
||||
}
|
||||
|
||||
def sendFd(fd: Int): Boolean = {
|
||||
if (fd != -1) {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000 * tries)
|
||||
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
|
||||
return true
|
||||
}
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
override def getTag = TAG
|
||||
|
||||
override def getServiceMode = Mode.VPN
|
||||
|
||||
@@ -66,7 +66,7 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
|
||||
case _: Exception => // ignore
|
||||
}
|
||||
serverSocket = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def stopThread() {
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import android.os.Handler
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
@@ -51,15 +50,10 @@ class TaskerReceiver extends BroadcastReceiver {
|
||||
val settings = TaskerSettings.fromIntent(intent)
|
||||
val switched = ShadowsocksApplication.profileManager.getProfile(settings.profileId) match {
|
||||
case Some(p) =>
|
||||
Utils.stopSsService(context)
|
||||
ShadowsocksApplication.switchProfile(settings.profileId)
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
val handler = new Handler(context.getMainLooper)
|
||||
handler.postDelayed(() => {
|
||||
if (settings.switchOn) Utils.startSsService(context)
|
||||
else if (!switched) Utils.stopSsService(context)
|
||||
}, 3000)
|
||||
if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.util.Base64
|
||||
import com.j256.ormlite.field.{DataType, DatabaseField}
|
||||
|
||||
class Profile {
|
||||
@@ -95,4 +98,7 @@ class Profile {
|
||||
|
||||
@DatabaseField
|
||||
var userOrder: Long = _
|
||||
|
||||
override def toString = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
|
||||
method, password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ package com.github
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Success, Failure, Try}
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
package object shadowsocks {
|
||||
|
||||
val handleFailure: PartialFunction[Try[_], Unit] = {
|
||||
case Success(_) =>
|
||||
private val handleFailure: Try[_] => Unit = {
|
||||
case Failure(e) => e.printStackTrace()
|
||||
case _ =>
|
||||
}
|
||||
|
||||
def ThrowableFuture[T](f: => T) = Future(f) onComplete handleFailure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ package com.github.shadowsocks.utils
|
||||
* @author Mygod
|
||||
*/
|
||||
object CloseUtils {
|
||||
type Closeable = {
|
||||
def close()
|
||||
}
|
||||
type Disconnectable = {
|
||||
def disconnect()
|
||||
}
|
||||
|
||||
def autoClose[A <: AutoCloseable, B](x: => A)(block: A => B): B = {
|
||||
def autoClose[A <: Closeable, B](x: => A)(block: A => B): B = {
|
||||
var a: Option[A] = None
|
||||
try {
|
||||
a = Some(x)
|
||||
|
||||
@@ -72,14 +72,16 @@ object Key {
|
||||
val isProxyApps = "isProxyApps"
|
||||
val isBypassApps = "isBypassApps"
|
||||
val isUdpDns = "isUdpDns"
|
||||
val isAuth= "isAuth"
|
||||
val isIpv6= "isIpv6"
|
||||
val isAuth = "isAuth"
|
||||
val isIpv6 = "isIpv6"
|
||||
|
||||
val proxy = "proxy"
|
||||
val sitekey = "sitekey"
|
||||
val encMethod = "encMethod"
|
||||
val remotePort = "remotePortNum"
|
||||
val localPort = "localPortNum"
|
||||
|
||||
val profileTip = "profileTip"
|
||||
}
|
||||
|
||||
object Scheme {
|
||||
@@ -104,6 +106,7 @@ object State {
|
||||
object Action {
|
||||
val SERVICE = "com.github.shadowsocks.SERVICE"
|
||||
val CLOSE = "com.github.shadowsocks.CLOSE"
|
||||
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
|
||||
}
|
||||
|
||||
object Route {
|
||||
|
||||
@@ -39,44 +39,30 @@
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.util.{Base64, Log}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import java.util.Locale
|
||||
import android.net.Uri
|
||||
import android.util.{Log, Base64}
|
||||
|
||||
object Parser {
|
||||
val TAG = "ShadowParser"
|
||||
private val pattern = "(?i)ss://([A-Za-z0-9+-/=_]+)".r
|
||||
|
||||
def generate (profile: Profile): String = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
|
||||
profile.method, profile.password, profile.host, profile.remotePort).getBytes, Base64.NO_PADDING)
|
||||
def findAll(data: CharSequence) = pattern.findAllMatchIn(data).map(m => try {
|
||||
val content = new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")
|
||||
val methodIdx = content.indexOf(':')
|
||||
val passwordIdx = content.lastIndexOf('@')
|
||||
val hostIdx = content.lastIndexOf(':')
|
||||
val host = content.substring(passwordIdx + 1, hostIdx)
|
||||
|
||||
def parse (data: String): Option[Profile] = {
|
||||
try {
|
||||
Log.d(TAG, data)
|
||||
val uri = Uri.parse(data.trim)
|
||||
if (uri.getScheme == Scheme.SS) {
|
||||
val encoded = data.replace(Scheme.SS + "://", "")
|
||||
val content = new String(Base64.decode(encoded, Base64.NO_PADDING), "UTF-8")
|
||||
val methodIdx = content.indexOf(':')
|
||||
val passwordIdx = content.lastIndexOf('@')
|
||||
val hostIdx = content.lastIndexOf(':')
|
||||
val method = content.substring(0, methodIdx)
|
||||
val password = content.substring(methodIdx + 1, passwordIdx)
|
||||
val host = content.substring(passwordIdx + 1, hostIdx)
|
||||
val port = content.substring(hostIdx + 1)
|
||||
|
||||
val profile = new Profile
|
||||
profile.name = host
|
||||
profile.host = host
|
||||
profile.remotePort = port.toInt
|
||||
profile.localPort = 1080
|
||||
profile.method = method.toLowerCase
|
||||
profile.password = password
|
||||
return Some(profile)
|
||||
}
|
||||
} catch {
|
||||
case ex : Exception => Log.e(TAG, "parser error", ex)// Ignore
|
||||
}
|
||||
None
|
||||
}
|
||||
val profile = new Profile
|
||||
profile.name = host
|
||||
profile.host = host
|
||||
profile.remotePort = content.substring(hostIdx + 1).toInt
|
||||
profile.method = content.substring(0, methodIdx).toLowerCase
|
||||
profile.password = content.substring(methodIdx + 1, passwordIdx)
|
||||
profile
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
Log.e(TAG, "parser error: " + m.source, ex)// Ignore
|
||||
null
|
||||
}).filter(_ != null)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import java.lang.System
|
||||
import java.text.DecimalFormat
|
||||
|
||||
import com.github.shadowsocks.{R, ShadowsocksApplication}
|
||||
@@ -81,4 +80,3 @@ object TrafficMonitor {
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.io._
|
||||
import java.net._
|
||||
import java.security.MessageDigest
|
||||
|
||||
import android.animation.{AnimatorListenerAdapter, Animator}
|
||||
import android.animation.{Animator, AnimatorListenerAdapter}
|
||||
import android.app.ActivityManager
|
||||
import android.content.pm.{ApplicationInfo, PackageManager}
|
||||
import android.content.{Context, Intent}
|
||||
@@ -51,11 +51,11 @@ import android.graphics.drawable.{BitmapDrawable, Drawable}
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.{DisplayMetrics, Base64, Log}
|
||||
import android.util.{Base64, DisplayMetrics, Log}
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.{Gravity, View, Window}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.{ShadowsocksRunnerService, ShadowsocksApplication, BuildConfig}
|
||||
import com.github.shadowsocks.{BuildConfig, ShadowsocksApplication, ShadowsocksRunnerService}
|
||||
import org.xbill.DNS._
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
def startSsService(context: Context): Unit = {
|
||||
def startSsService(context: Context) {
|
||||
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
|
||||
if (!isInstalled) return
|
||||
|
||||
@@ -409,8 +409,9 @@ object Utils {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
def stopSsService(context: Context): Unit = {
|
||||
context.sendBroadcast(new Intent(Action.CLOSE))
|
||||
def stopSsService(context: Context) {
|
||||
val intent = new Intent(Action.CLOSE)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ if [ ! -d "$ANDROID_NDK_HOME" ]; then
|
||||
popd
|
||||
fi
|
||||
|
||||
echo "y" | android update sdk --filter tools,platform-tools,build-tools-23.0.2,android-23,extra-google-m2repository --no-ui -a
|
||||
echo "y" | android update sdk --filter extra-android-m2repository --no-ui -a
|
||||
( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter tools,platform-tools,build-tools-23.0.2,android-23,extra-google-m2repository --no-ui -a
|
||||
( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter extra-android-m2repository --no-ui -a
|
||||
|
||||