Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac51aa112 | ||
|
|
d2f245fd2f | ||
|
|
7e6bc0d740 | ||
|
|
de768f7979 | ||
|
|
edd5fb4a4c | ||
|
|
4d81cacbdd | ||
|
|
38b333811e | ||
|
|
63569a4448 | ||
|
|
1023454266 | ||
|
|
4312e1676d | ||
|
|
3f93ad079b | ||
|
|
ad831aa1f4 | ||
|
|
93084f0ed0 | ||
|
|
d27407003c | ||
|
|
9541ac0111 | ||
|
|
ac98caa4b4 | ||
|
|
8c2690c0fb | ||
|
|
c6a7560bae | ||
|
|
dd4206613f | ||
|
|
348c7b6bd1 | ||
|
|
37d05236be | ||
|
|
faa1e5def4 | ||
|
|
d2a1027229 | ||
|
|
b0f3d91d5d | ||
|
|
873560be7e | ||
|
|
66f8252326 | ||
|
|
362ec7ebc8 | ||
|
|
a27336ae67 | ||
|
|
0ca3d2c8dc | ||
|
|
6725bcc8da | ||
|
|
e5ce8fd0a6 | ||
|
|
56a065b182 | ||
|
|
41b7da4b63 | ||
|
|
b0180fb9a1 | ||
|
|
ecfbc3e1a7 | ||
|
|
253cfec33d | ||
|
|
a56fb2b522 | ||
|
|
52eb2607f9 | ||
|
|
624ccba559 | ||
|
|
7e8a5ac9df | ||
|
|
e524ab8d2e | ||
|
|
c969aed4da | ||
|
|
f891bd1789 | ||
|
|
a6c12dfd5e | ||
|
|
3d998a326c | ||
|
|
1de3a4497c | ||
|
|
b11280479b | ||
|
|
2127e17cc7 | ||
|
|
d9179fcc9a | ||
|
|
b3d188fbae | ||
|
|
c6d2a44c15 | ||
|
|
2151873264 | ||
|
|
9b2671d565 | ||
|
|
f728a67691 | ||
|
|
b269d9a649 | ||
|
|
1eda4c47a4 | ||
|
|
c1730e4fa6 | ||
|
|
9fa57dfe4b | ||
|
|
5e0d52324b | ||
|
|
118aa1271c | ||
|
|
99a6c4ff3e | ||
|
|
19c4910794 | ||
|
|
8b0b39cba7 | ||
|
|
be8bc523ad | ||
|
|
050007b49b |
@@ -41,7 +41,7 @@ Help to translate shadowsocks: http://crowdin.net/project/shadowsocks/invite
|
||||
* Install Android SDK and NDK by run `brew install android-ndk android-sdk`
|
||||
* Add `export ANDROID_HOME=/usr/local/Cellar/android-sdk/$version` to your .bashrc , then reopen the shell to load it.
|
||||
* Add `export ANDROID_NDK_HOME=/usr/local/Cellar/android-ndk/$version` to your .bashrc , then reopen the shell to load it.
|
||||
* echo "y" | android update sdk --filter tools,platform-tools,build-tools-21.0.1,android-21,extra-google-m2repository --no-ui --no-https -a
|
||||
* 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 --no-https -a
|
||||
* Create your key following the instructions at http://developer.android.com/guide/publishing/app-signing.html#cert
|
||||
* Put your key in ~/.keystore
|
||||
|
||||
@@ -30,6 +30,7 @@ libraryDependencies ++= Seq(
|
||||
"dnsjava" % "dnsjava" % "2.1.7",
|
||||
"com.github.kevinsawicki" % "http-request" % "6.0",
|
||||
"commons-net" % "commons-net" % "3.3",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201510071325",
|
||||
"com.google.zxing" % "android-integration" % "3.2.1",
|
||||
"net.glxn.qrgen" % "android" % "2.0",
|
||||
"com.google.android.gms" % "play-services-base" % "8.3.0",
|
||||
@@ -38,7 +39,8 @@ libraryDependencies ++= Seq(
|
||||
"com.android.support" % "design" % "23.1.1",
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
|
||||
"com.j256.ormlite" % "ormlite-core" % "4.48",
|
||||
"com.j256.ormlite" % "ormlite-android" % "4.48"
|
||||
"com.j256.ormlite" % "ormlite-android" % "4.48",
|
||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2"
|
||||
)
|
||||
|
||||
proguardOptions in Android ++= Seq("-keep class com.github.shadowsocks.** { <init>(...); }",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
sbt.version=0.13.8
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
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.9")
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.10")
|
||||
|
||||
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.7.0-SNAPSHOT")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.shadowsocks"
|
||||
android:versionCode="122"
|
||||
android:versionName="2.9.0">
|
||||
android:versionCode="124"
|
||||
android:versionName="2.9.2">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
@@ -80,6 +80,13 @@
|
||||
android:parentActivityName=".Shadowsocks"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".TaskerActivity"
|
||||
android:icon="@drawable/ic_launcher">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".ShadowsocksRunnerService"
|
||||
android:exported="false">
|
||||
@@ -102,6 +109,14 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".AppManagerReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
<data android:scheme="package"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".ShadowsocksReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
@@ -109,6 +124,11 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".TaskerReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Base application class to extend from, solving some issues with
|
||||
* toasts and AsyncTasks you are likely to run into
|
||||
*/
|
||||
public class Application extends android.app.Application {
|
||||
/**
|
||||
* Shows a toast message
|
||||
*
|
||||
* @param context Any context belonging to this application
|
||||
* @param message The message to show
|
||||
*/
|
||||
public static void toast(Context context, String message) {
|
||||
// this is a static method so it is easier to call,
|
||||
// as the context checking and casting is done for you
|
||||
|
||||
if (context == null) return;
|
||||
|
||||
if (!(context instanceof Application)) {
|
||||
context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
if (context instanceof Application) {
|
||||
final Context c = context;
|
||||
final String m = message;
|
||||
|
||||
((Application)context).runInApplicationThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(c, m, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static Handler mApplicationHandler = new Handler();
|
||||
|
||||
/**
|
||||
* Run a runnable in the main application thread
|
||||
*
|
||||
* @param r Runnable to run
|
||||
*/
|
||||
public void runInApplicationThread(Runnable r) {
|
||||
mApplicationHandler.post(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
try {
|
||||
// workaround bug in AsyncTask, can show up (for example) when you toast from a service
|
||||
// this makes sure AsyncTask's internal handler is created from the right (main) thread
|
||||
Class.forName("android.os.AsyncTask");
|
||||
} catch (ClassNotFoundException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.github.shadowsocks.BuildConfig;
|
||||
|
||||
/**
|
||||
* Utility class for logging and debug features that (by default) does nothing when not in debug mode
|
||||
*/
|
||||
public class Debug {
|
||||
|
||||
// ----- DEBUGGING -----
|
||||
|
||||
private static boolean debug = BuildConfig.DEBUG;
|
||||
|
||||
/**
|
||||
* <p>Enable or disable debug mode</p>
|
||||
*
|
||||
* <p>By default, debug mode is enabled for development
|
||||
* builds and disabled for exported APKs - see
|
||||
* BuildConfig.DEBUG</p>
|
||||
*
|
||||
* @param enabled Enable debug mode ?
|
||||
*/
|
||||
public static void setDebug(boolean enable) {
|
||||
debug = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Is debug mode enabled ?</p>
|
||||
*
|
||||
* @return Debug mode enabled
|
||||
*/
|
||||
public static boolean getDebug() {
|
||||
return debug;
|
||||
}
|
||||
|
||||
// ----- LOGGING -----
|
||||
|
||||
public interface OnLogListener {
|
||||
public void onLog(int type, String typeIndicator, String message);
|
||||
}
|
||||
|
||||
public static final String TAG = "libsuperuser";
|
||||
|
||||
public static final int LOG_GENERAL = 0x0001;
|
||||
public static final int LOG_COMMAND = 0x0002;
|
||||
public static final int LOG_OUTPUT = 0x0004;
|
||||
|
||||
public static final int LOG_NONE = 0x0000;
|
||||
public static final int LOG_ALL = 0xFFFF;
|
||||
|
||||
private static int logTypes = LOG_ALL;
|
||||
|
||||
private static OnLogListener logListener = null;
|
||||
|
||||
/**
|
||||
* <p>Log a message (internal)</p>
|
||||
*
|
||||
* <p>Current debug and enabled logtypes decide what gets logged -
|
||||
* even if a custom callback is registered</p>
|
||||
*
|
||||
* @param type Type of message to log
|
||||
* @param typeIndicator String indicator for message type
|
||||
* @param message The message to log
|
||||
*/
|
||||
private static void logCommon(int type, String typeIndicator, String message) {
|
||||
if (debug && ((logTypes & type) == type)) {
|
||||
if (logListener != null) {
|
||||
logListener.onLog(type, typeIndicator, message);
|
||||
} else {
|
||||
Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Log a "general" message</p>
|
||||
*
|
||||
* <p>These messages are infrequent and mostly occur at startup/shutdown or on error</p>
|
||||
*
|
||||
* @param message The message to log
|
||||
*/
|
||||
public static void log(String message) {
|
||||
logCommon(LOG_GENERAL, "G", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Log a "per-command" message</p>
|
||||
*
|
||||
* <p>This could produce a lot of output if the client runs many commands in the session</p>
|
||||
*
|
||||
* @param message The message to log
|
||||
*/
|
||||
public static void logCommand(String message) {
|
||||
logCommon(LOG_COMMAND, "C", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Log a line of stdout/stderr output</p>
|
||||
*
|
||||
* <p>This could produce a lot of output if the shell commands are noisy</p>
|
||||
*
|
||||
* @param message The message to log
|
||||
*/
|
||||
public static void logOutput(String message) {
|
||||
logCommon(LOG_OUTPUT, "O", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Enable or disable logging specific types of message</p>
|
||||
*
|
||||
* <p>You may | (or) LOG_* constants together. Note that
|
||||
* debug mode must also be enabled for actual logging to
|
||||
* occur.</p>
|
||||
*
|
||||
* @param type LOG_* constants
|
||||
* @param enabled Enable or disable
|
||||
*/
|
||||
public static void setLogTypeEnabled(int type, boolean enable) {
|
||||
if (enable) {
|
||||
logTypes |= type;
|
||||
} else {
|
||||
logTypes &= ~type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Is logging for specific types of messages enabled ?</p>
|
||||
*
|
||||
* <p>You may | (or) LOG_* constants together, to learn if
|
||||
* <b>all</b> passed message types are enabled for logging. Note
|
||||
* that debug mode must also be enabled for actual logging
|
||||
* to occur.</p>
|
||||
*
|
||||
* @param type LOG_* constants
|
||||
*/
|
||||
public static boolean getLogTypeEnabled(int type) {
|
||||
return ((logTypes & type) == type);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Is logging for specific types of messages enabled ?</p>
|
||||
*
|
||||
* <p>You may | (or) LOG_* constants together, to learn if
|
||||
* <b>all</b> message types are enabled for logging. Takes
|
||||
* debug mode into account for the result.</p>
|
||||
*
|
||||
* @param type LOG_* constants
|
||||
*/
|
||||
public static boolean getLogTypeEnabledEffective(int type) {
|
||||
return getDebug() && getLogTypeEnabled(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Register a custom log handler</p>
|
||||
*
|
||||
* <p>Replaces the log method (write to logcat) with your own
|
||||
* handler. Whether your handler gets called is still dependent
|
||||
* on debug mode and message types being enabled for logging.</p>
|
||||
*
|
||||
* @param onLogListener Custom log listener or NULL to revert to default
|
||||
*/
|
||||
public static void setOnLogListener(OnLogListener onLogListener) {
|
||||
logListener = onLogListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Get the currently registered custom log handler</p>
|
||||
*
|
||||
* @return Current custom log handler or NULL if none is present
|
||||
*/
|
||||
public static OnLogListener getOnLogListener() {
|
||||
return logListener;
|
||||
}
|
||||
|
||||
// ----- SANITY CHECKS -----
|
||||
|
||||
private static boolean sanityChecks = true;
|
||||
|
||||
/**
|
||||
* <p>Enable or disable sanity checks</p>
|
||||
*
|
||||
* <p>Enables or disables the library crashing when su is called
|
||||
* from the main thread.</p>
|
||||
*
|
||||
* @param enabled Enable or disable
|
||||
*/
|
||||
public static void setSanityChecksEnabled(boolean enable) {
|
||||
sanityChecks = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Are sanity checks enabled ?</p>
|
||||
*
|
||||
* <p>Note that debug mode must also be enabled for actual
|
||||
* sanity checks to occur.</p>
|
||||
*
|
||||
* @return True if enabled
|
||||
*/
|
||||
public static boolean getSanityChecksEnabled() {
|
||||
return sanityChecks;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Are sanity checks enabled ?</p>
|
||||
*
|
||||
* <p>Takes debug mode into account for the result.</p>
|
||||
*
|
||||
* @return True if enabled
|
||||
*/
|
||||
public static boolean getSanityChecksEnabledEffective() {
|
||||
return getDebug() && getSanityChecksEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Are we running on the main thread ?</p>
|
||||
*
|
||||
* @return Running on main thread ?
|
||||
*/
|
||||
public static boolean onMainThread() {
|
||||
return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Base receiver to extend to catch notifications when overlays should be
|
||||
* hidden.
|
||||
* </p>
|
||||
* <p>
|
||||
* Tapjacking protection in SuperSU prevents some dialogs from receiving user
|
||||
* input when overlays are present. For security reasons this notification is
|
||||
* only sent to apps that have previously been granted root access, so even if
|
||||
* your app does not <em>require</em> root, you still need to <em>request</em>
|
||||
* it, and the user must grant it.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that the word overlay as used here should be interpreted as "any view or
|
||||
* window possibly obscuring SuperSU dialogs".
|
||||
* </p>
|
||||
*/
|
||||
public abstract class HideOverlaysReceiver extends BroadcastReceiver {
|
||||
public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS";
|
||||
public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO;
|
||||
public static final String EXTRA_HIDE_OVERLAYS = "eu.chainfire.supersu.extra.HIDE";
|
||||
|
||||
@Override
|
||||
public final void onReceive(Context context, Intent intent) {
|
||||
if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) {
|
||||
onHideOverlays(intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when overlays <em>should</em> be hidden or <em>may</em> be shown
|
||||
* again.
|
||||
*
|
||||
* @param hide Should overlays be hidden?
|
||||
*/
|
||||
public abstract void onHideOverlays(boolean hide);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
/**
|
||||
* Exception class used to notify developer that a shell was not close()d
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ShellNotClosedException extends RuntimeException {
|
||||
public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell";
|
||||
|
||||
public ShellNotClosedException() {
|
||||
super(EXCEPTION_NOT_CLOSED);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
/**
|
||||
* Exception class used to crash application when shell commands are executed
|
||||
* from the main thread, and we are in debug mode.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ShellOnMainThreadException extends RuntimeException {
|
||||
public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread";
|
||||
public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread";
|
||||
public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread";
|
||||
|
||||
public ShellOnMainThreadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.chainfire.libsuperuser;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Thread utility class continuously reading from an InputStream
|
||||
*/
|
||||
public class StreamGobbler extends Thread {
|
||||
/**
|
||||
* Line callback interface
|
||||
*/
|
||||
public interface OnLineListener {
|
||||
/**
|
||||
* <p>Line callback</p>
|
||||
*
|
||||
* <p>This callback should process the line as quickly as possible.
|
||||
* Delays in this callback may pause the native process or even
|
||||
* result in a deadlock</p>
|
||||
*
|
||||
* @param line String that was gobbled
|
||||
*/
|
||||
public void onLine(String line);
|
||||
}
|
||||
|
||||
private String shell = null;
|
||||
private BufferedReader reader = null;
|
||||
private List<String> writer = null;
|
||||
private OnLineListener listener = null;
|
||||
|
||||
/**
|
||||
* <p>StreamGobbler constructor</p>
|
||||
*
|
||||
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
|
||||
* possible to prevent a deadlock from occurring, or Process.waitFor() never
|
||||
* returning (as the buffer is full, pausing the native process)</p>
|
||||
*
|
||||
* @param shell Name of the shell
|
||||
* @param inputStream InputStream to read from
|
||||
* @param outputList List<String> to write to, or null
|
||||
*/
|
||||
public StreamGobbler(String shell, InputStream inputStream, List<String> outputList) {
|
||||
this.shell = shell;
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
writer = outputList;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>StreamGobbler constructor</p>
|
||||
*
|
||||
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
|
||||
* possible to prevent a deadlock from occurring, or Process.waitFor() never
|
||||
* returning (as the buffer is full, pausing the native process)</p>
|
||||
*
|
||||
* @param shell Name of the shell
|
||||
* @param inputStream InputStream to read from
|
||||
* @param onLineListener OnLineListener callback
|
||||
*/
|
||||
public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) {
|
||||
this.shell = shell;
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
listener = onLineListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// keep reading the InputStream until it ends (or an error occurs)
|
||||
try {
|
||||
String line = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
Debug.logOutput(String.format("[%s] %s", shell, line));
|
||||
if (writer != null) writer.add(line);
|
||||
if (listener != null) listener.onLine(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
|
||||
// make sure our stream is closed and resources will be freed
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ SHADOWSOCKS_SOURCES := local.c cache.c udprelay.c encrypt.c utils.c netutils.c j
|
||||
|
||||
LOCAL_MODULE := ss-local
|
||||
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DUDPRELAY_LOCAL \
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_LOCAL \
|
||||
-DUSE_CRYPTO_OPENSSL -DANDROID -DHAVE_CONFIG_H \
|
||||
-I$(LOCAL_PATH)/libev \
|
||||
-I$(LOCAL_PATH)/libancillary \
|
||||
@@ -281,7 +281,7 @@ SHADOWSOCKS_SOURCES := tunnel.c cache.c udprelay.c encrypt.c utils.c netutils.c
|
||||
|
||||
LOCAL_MODULE := ss-tunnel
|
||||
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DUDPRELAY_LOCAL -DUDPRELAY_TUNNEL \
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_TUNNEL \
|
||||
-DUSE_CRYPTO_OPENSSL -DANDROID -DHAVE_CONFIG_H -DSSTUNNEL_JNI \
|
||||
-I$(LOCAL_PATH)/libev \
|
||||
-I$(LOCAL_PATH)/libancillary \
|
||||
|
||||
Submodule src/main/jni/shadowsocks-libev updated: d462e4cd8d...f3428e621a
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 221 B |
Binary file not shown.
|
After Width: | Height: | Size: 175 B |
Binary file not shown.
|
After Width: | Height: | Size: 257 B |
Binary file not shown.
|
After Width: | Height: | Size: 347 B |
Binary file not shown.
|
After Width: | Height: | Size: 436 B |
@@ -9,48 +9,34 @@
|
||||
<include layout="@layout/toolbar_light_dark"/>
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="48dp"
|
||||
android:paddingStart="48dp"
|
||||
android:elevation="1dp"
|
||||
android:background="@android:color/white"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlSymmetry">
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
<Switch android:checked="true"
|
||||
android:id="@+id/onSwitch"
|
||||
android:text="@string/on"
|
||||
android:textSize="18sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:paddingLeft="60dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/on"/>
|
||||
<Switch android:checked="true"
|
||||
android:id="@+id/onSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
<Switch android:checked="true"
|
||||
android:id="@+id/bypassSwitch"
|
||||
android:text="@string/bypass_apps"
|
||||
android:textSize="18sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:paddingLeft="60dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
<TextView
|
||||
android:id="@+id/bypassLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/bypass_apps"/>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/bypassSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<include layout="@layout/toolbar_light_dark" />
|
||||
<FrameLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="1dp"
|
||||
android:background="@android:color/white">
|
||||
<Switch android:id="@+id/serviceSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/toggle_service_state"
|
||||
android:textSize="18sp"
|
||||
android:padding="16dp"/>
|
||||
</FrameLayout>
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
@@ -32,11 +32,10 @@
|
||||
<string name="bypass_apps_summary">启用该选项,以使所选应用程序的流量不经过代理</string>
|
||||
<string name="auto_connect">自动连接</string>
|
||||
<string name="auto_connect_summary">随系统启动后台服务</string>
|
||||
<string name="traffic_stat">流量统计</string>
|
||||
<string name="traffic_stat_summary">在通知栏上显示当前上下行速率</string>
|
||||
<string name="forward_success">后台服务已开始运行。</string>
|
||||
<string name="service_running">已连接到「%s」</string>
|
||||
<string name="service_failed">无法连接远程服务器</string>
|
||||
<string name="nat_no_root">NAT 模式需要 ROOT 权限</string>
|
||||
<string name="switch_to_vpn">切换到 VPN 模式</string>
|
||||
<string name="stop">停止服务</string>
|
||||
<string name="stopping">正在关闭……</string>
|
||||
<string name="vpn_error">后台服务启动失败:%s</string>
|
||||
@@ -55,14 +54,15 @@
|
||||
<string name="about">关于</string>
|
||||
<string name="about_title">影梭(shadowsocks)%s</string>
|
||||
<string name="flush_dnscache">刷新 DNS 缓存</string>
|
||||
<string name="flush_dnscache_summary_disabled">需要 ROOT 权限</string>
|
||||
<string name="flush_dnscache_summary">只需在 NAT 模式下使用</string>
|
||||
<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="add_profile">添加配置文件</string>
|
||||
|
||||
<string name="nat">NAT 模式 (仅限调试)</string>
|
||||
<string name="nat_summary">从 VPN 模式切换为 NAT 模式</string>
|
||||
<string name="nat_summary_no_root">NAT 模式已禁用,需要 ROOT 权限</string>
|
||||
<string name="nat_summary">从 VPN 模式切换为 NAT 模式,需要 ROOT 权限</string>
|
||||
|
||||
<string name="udp_dns">UDP 转发</string>
|
||||
<string name="udp_dns_summary">由远程服务器转发 UDP 协议的数据包</string>
|
||||
@@ -99,5 +99,10 @@
|
||||
|
||||
<string name="removed">已删除</string>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="toggle_service_state">启动服务</string>
|
||||
<string name="start_service_default">连接到当前服务器</string>
|
||||
<string name="start_service">连接到 %s</string>
|
||||
<string name="stop_service">切换到 %s</string>
|
||||
<string name="profile_default">使用当前配置</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="SummaryPreference">
|
||||
<attr name="summary" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="DropDownPreference">
|
||||
<attr name="entries" format="reference" />
|
||||
<attr name="entryValues" format="reference" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<color name="material_green_700">#388E3C</color>
|
||||
<color name="material_green_a700">#64DD17</color>
|
||||
<color name="material_blue_grey_300">#90A4AE</color>
|
||||
<color name="material_blue_grey_500">#607D8B</color>
|
||||
<color name="material_blue_grey_700">#455A64</color>
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<string name="profile">Profile</string>
|
||||
<string name="profile_summary">Switch to another profile or add new profiles</string>
|
||||
<string name="nat">NAT mode (deprecated)</string>
|
||||
<string name="nat_summary">Use NAT mode instead of VPN mode</string>
|
||||
<string name="nat_summary_no_root">NAT mode is disabled without root</string>
|
||||
<string name="nat_summary">Use NAT mode instead of VPN mode. Requires ROOT permission.</string>
|
||||
<string name="stat">Network Traffic</string>
|
||||
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s</string>
|
||||
<string name="stat_profiles">\n%1$s↑\t%2$s↓</string>
|
||||
<string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="one">Byte</item>
|
||||
<item quantity="other">Bytes</item>
|
||||
@@ -54,15 +54,14 @@
|
||||
<string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
|
||||
<string name="auto_connect">Auto Connect</string>
|
||||
<string name="auto_connect_summary">Enable Shadowsocks on startup</string>
|
||||
<string name="traffic_stat">Traffic Stats</string>
|
||||
<string name="traffic_stat_summary">Show traffic stats in notification bar</string>
|
||||
<string name="udp_dns">UDP Forwarding</string>
|
||||
<string name="udp_dns_summary">Forward UDP packets to remote</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">Shadowsocks started.</string>
|
||||
<string name="service_running">Connected to "%s"</string>
|
||||
<string name="service_failed">Failed to connect the remote server</string>
|
||||
<string name="nat_no_root">NAT mode requires ROOT permission</string>
|
||||
<string name="switch_to_vpn">Switch to VPN mode</string>
|
||||
<string name="stop">Stop the service</string>
|
||||
<string name="stopping">Shutting down…</string>
|
||||
<string name="vpn_error">%s</string>
|
||||
@@ -86,7 +85,9 @@
|
||||
<string name="about_title">Shadowsocks %s</string>
|
||||
<string name="qrcode">QR Code</string>
|
||||
<string name="flush_dnscache">Flush DNS cache</string>
|
||||
<string name="flush_dnscache_summary_disabled">Requires ROOT permission</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>
|
||||
<string name="flush_dnscache_no_root">Flush DNS cache cannot proceed without ROOT permission</string>
|
||||
<string name="flushing">Flushing</string>
|
||||
<string name="add_profile">Add Profile</string>
|
||||
<string name="action_export">Export to Clipboard</string>
|
||||
@@ -103,4 +104,10 @@
|
||||
<string name="removed">Removed</string>
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
<!-- tasker -->
|
||||
<string name="toggle_service_state">Start the service</string>
|
||||
<string name="start_service_default">Connect to the current server</string>
|
||||
<string name="start_service">Connect to %s</string>
|
||||
<string name="stop_service">Switch to %s</string>
|
||||
<string name="profile_default">Use the current profile</string>
|
||||
</resources>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<resources>
|
||||
|
||||
<style name="Toolbar.Logo" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">24sp</item>
|
||||
<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>
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
<com.github.shadowsocks.preferences.NumberPickerPreference
|
||||
app:min="1"
|
||||
app:max="65535"
|
||||
android:key="remotePort"
|
||||
app:summary="@string/remote_port_summary"
|
||||
android:key="remotePortNum"
|
||||
android:summary="@string/remote_port_summary"
|
||||
android:title="@string/remote_port"/>
|
||||
<com.github.shadowsocks.preferences.NumberPickerPreference
|
||||
app:min="1025"
|
||||
app:max="65535"
|
||||
android:key="port"
|
||||
app:summary="@string/port_summary"
|
||||
android:key="localPortNum"
|
||||
android:summary="@string/port_summary"
|
||||
android:title="@string/port"/>
|
||||
<com.github.shadowsocks.preferences.PasswordEditTextPreference
|
||||
android:inputType="textPassword"
|
||||
@@ -43,7 +43,7 @@
|
||||
android:key="encMethod"
|
||||
app:entries="@array/enc_method_entry"
|
||||
app:entryValues="@array/enc_method_value"
|
||||
app:summary="%s"
|
||||
android:summary="%s"
|
||||
android:title="@string/enc_method"/>
|
||||
<SwitchPreference
|
||||
android:key="isAuth"
|
||||
@@ -59,7 +59,7 @@
|
||||
android:key="route"
|
||||
app:entries="@array/route_entry"
|
||||
app:entryValues="@array/route_value"
|
||||
app:summary="%s"
|
||||
android:summary="%s"
|
||||
android:title="@string/route_list"/>
|
||||
<SwitchPreference
|
||||
android:key="isIpv6"
|
||||
@@ -83,12 +83,14 @@
|
||||
android:summary="@string/auto_connect_summary"
|
||||
android:title="@string/auto_connect"/>
|
||||
|
||||
<com.github.shadowsocks.preferences.NatSwitchPreference android:key="isNAT"
|
||||
android:title="@string/nat"
|
||||
android:summary="@string/nat_summary"/>
|
||||
<SwitchPreference android:key="isNAT"
|
||||
android:title="@string/nat"
|
||||
android:summary="@string/nat_summary"/>
|
||||
<Preference android:key="recovery" android:title="@string/recovery"
|
||||
android:summary="@string/recovery_summary"/>
|
||||
<Preference android:key="flush_dnscache" android:title="@string/flush_dnscache"/>
|
||||
<Preference android:key="flush_dnscache"
|
||||
android:title="@string/flush_dnscache"
|
||||
android:summary="@string/flush_dnscache_summary_root"/>
|
||||
<Preference android:key="about" android:title="@string/about"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.{ClipData, ClipboardManager, Context, SharedPreferences}
|
||||
import android.content.{ClipData, ClipboardManager, Context}
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.{Bundle, Handler}
|
||||
@@ -50,101 +50,50 @@ import android.support.v7.widget.Toolbar
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.{MenuItem, View, ViewGroup, WindowManager}
|
||||
import android.view._
|
||||
import android.widget.AbsListView.OnScrollListener
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget._
|
||||
import android.Manifest.permission
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.mutable
|
||||
import scala.language.implicitConversions
|
||||
|
||||
case class ProxiedApp(uid: Int, name: String, packageName: String, icon: Drawable, var proxied: Boolean)
|
||||
|
||||
class ObjectArrayTools[T <: AnyRef](a: Array[T]) {
|
||||
def binarySearch(key: T) = {
|
||||
java.util.Arrays.binarySearch(a.asInstanceOf[Array[AnyRef]], key)
|
||||
}
|
||||
}
|
||||
|
||||
case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
|
||||
|
||||
object AppManager {
|
||||
case class ProxiedApp(name: String, packageName: String, icon: Drawable)
|
||||
private case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
|
||||
|
||||
implicit def anyrefarray_tools[T <: AnyRef](a: Array[T]): ObjectArrayTools[T] = new ObjectArrayTools(a)
|
||||
|
||||
def getProxiedApps(context: Context, proxiedAppString: String): Array[ProxiedApp] = {
|
||||
|
||||
val proxiedApps = proxiedAppString.split('|').sortWith(_ < _)
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
val packageManager: PackageManager = context.getPackageManager
|
||||
val appList = packageManager.getInstalledApplications(0)
|
||||
|
||||
appList.filter(_.uid >= 10000).map {
|
||||
case a =>
|
||||
val uid = a.uid
|
||||
val userName = uid.toString
|
||||
val name = packageManager.getApplicationLabel(a).toString
|
||||
val packageName = a.packageName
|
||||
val proxied = proxiedApps.binarySearch(userName) >= 0
|
||||
new ProxiedApp(uid, name, packageName, null, proxied)
|
||||
}.toArray
|
||||
var cachedApps: Array[ProxiedApp] = _
|
||||
private def getApps(pm: PackageManager) = {
|
||||
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
|
||||
.map(p => new ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
|
||||
p.applicationInfo.loadIcon(pm))).toArray
|
||||
cachedApps
|
||||
}
|
||||
}
|
||||
|
||||
class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnClickListener
|
||||
with OnMenuItemClickListener {
|
||||
import AppManager._
|
||||
|
||||
val MSG_LOAD_START = 1
|
||||
val MSG_LOAD_FINISH = 2
|
||||
val STUB = android.R.drawable.sym_def_app_icon
|
||||
|
||||
implicit def anyrefarray_tools[T <: AnyRef](a: Array[T]): ObjectArrayTools[T] = new ObjectArrayTools(a)
|
||||
|
||||
var apps: Array[ProxiedApp] = _
|
||||
var appListView: ListView = _
|
||||
var loadingView: View = _
|
||||
var overlay: TextView = _
|
||||
var adapter: ListAdapter = _
|
||||
@volatile var appsLoading: Boolean = _
|
||||
|
||||
def loadApps(context: Context): Array[ProxiedApp] = {
|
||||
val proxiedAppString = ShadowsocksApplication.settings.getString(Key.proxied, "")
|
||||
val proxiedApps = proxiedAppString.split('|').sortWith(_ < _)
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
val packageManager: PackageManager = context.getPackageManager
|
||||
val appList = packageManager.getInstalledApplications(0)
|
||||
|
||||
appList.filter(a => a.uid >= 10000
|
||||
&& packageManager.getApplicationLabel(a) != null
|
||||
&& packageManager.getApplicationIcon(a) != null).map {
|
||||
a =>
|
||||
val uid = a.uid
|
||||
val userName = uid.toString
|
||||
val name = packageManager.getApplicationLabel(a).toString
|
||||
val packageName = a.packageName
|
||||
val proxied = (proxiedApps binarySearch userName) >= 0
|
||||
new ProxiedApp(uid, name, packageName, a.loadIcon(packageManager), proxied)
|
||||
}.toArray
|
||||
}
|
||||
private var apps: Array[ProxiedApp] = _
|
||||
private var proxiedApps: mutable.HashSet[String] = _
|
||||
private var toolbar: Toolbar = _
|
||||
private var appListView: ListView = _
|
||||
private var loadingView: View = _
|
||||
private var overlay: TextView = _
|
||||
private var adapter: ListAdapter = _
|
||||
@volatile private var appsLoading: Boolean = _
|
||||
|
||||
def loadApps() {
|
||||
appsLoading = true
|
||||
apps = loadApps(this).sortWith((a, b) => {
|
||||
if (a == null || b == null || a.name == null || b.name == null) {
|
||||
true
|
||||
} else if (a.proxied == b.proxied) {
|
||||
a.name < b.name
|
||||
} else if (a.proxied) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
|
||||
apps = getApps(getPackageManager).sortWith((a, b) => {
|
||||
val aProxied = proxiedApps.contains(a.packageName)
|
||||
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
|
||||
})
|
||||
adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) {
|
||||
override def getView(position: Int, view: View, parent: ViewGroup): View = {
|
||||
@@ -152,10 +101,9 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
var entry: ListEntry = null
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false)
|
||||
val icon = convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView]
|
||||
val switch = convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
|
||||
val text = convertView.findViewById(R.id.itemtext).asInstanceOf[TextView]
|
||||
entry = new ListEntry(switch, text, icon)
|
||||
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
|
||||
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
|
||||
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
|
||||
convertView.setOnClickListener(AppManager.this)
|
||||
convertView.setTag(entry)
|
||||
entry.switch.setOnCheckedChangeListener(AppManager.this)
|
||||
@@ -169,19 +117,19 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
entry.icon.setImageDrawable(app.icon)
|
||||
val switch = entry.switch
|
||||
switch.setTag(app)
|
||||
switch.setChecked(app.proxied)
|
||||
switch.setChecked(proxiedApps.contains(app.packageName))
|
||||
entry.text.setTag(switch)
|
||||
convertView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def setProxied(pn: String, proxied: Boolean) = if (proxied) proxiedApps.add(pn) else proxiedApps.remove(pn)
|
||||
|
||||
/** Called an application is check/unchecked */
|
||||
def onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
val app: ProxiedApp = buttonView.getTag.asInstanceOf[ProxiedApp]
|
||||
if (app != null) {
|
||||
app.proxied = isChecked
|
||||
}
|
||||
if (app != null) setProxied(app.packageName, isChecked)
|
||||
saveAppSettings(this)
|
||||
}
|
||||
|
||||
@@ -189,8 +137,9 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
val switch = v.getTag.asInstanceOf[ListEntry].switch
|
||||
val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp]
|
||||
if (app != null) {
|
||||
app.proxied = !app.proxied
|
||||
switch.setChecked(app.proxied)
|
||||
val proxied = !proxiedApps.contains(app.packageName)
|
||||
setProxied(app.packageName, proxied)
|
||||
switch.setChecked(proxied)
|
||||
}
|
||||
saveAppSettings(this)
|
||||
}
|
||||
@@ -210,7 +159,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
case R.id.action_export =>
|
||||
val bypass = prefs.getBoolean(Key.isBypassApps, false)
|
||||
val proxiedAppString = prefs.getString(Key.proxied, "")
|
||||
val clip = ClipData.newPlainText(Key.proxied, bypass + " " + proxiedAppString)
|
||||
val clip = ClipData.newPlainText(Key.proxied, bypass + "\n" + proxiedAppString)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
@@ -223,11 +172,12 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
if (proxiedAppSequence != null) {
|
||||
val proxiedAppString = proxiedAppSequence.toString
|
||||
if (!proxiedAppString.isEmpty) {
|
||||
val array = proxiedAppString.split(" ")
|
||||
val bypass = array(0).toBoolean
|
||||
val apps = if (array.size > 1) array(1) else ""
|
||||
prefs.edit.putBoolean(Key.isBypassApps, bypass).apply()
|
||||
prefs.edit.putString(Key.proxied, apps).apply()
|
||||
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)
|
||||
@@ -250,7 +200,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
handler = new Handler()
|
||||
|
||||
this.setContentView(R.layout.layout_apps)
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(R.string.proxied_apps)
|
||||
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha)
|
||||
toolbar.setNavigationOnClickListener((v: View) => {
|
||||
@@ -285,12 +235,8 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
def onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int,
|
||||
totalItemCount: Int) {
|
||||
if (visible) {
|
||||
val name: String = apps(firstVisibleItem).name
|
||||
if (name != null && name.length > 1) {
|
||||
overlay.setText(apps(firstVisibleItem).name.substring(0, 1))
|
||||
} else {
|
||||
overlay.setText("*")
|
||||
}
|
||||
val name = apps(firstVisibleItem).name
|
||||
overlay.setText(if (name != null && name.length > 1) name(0).toUpper.toString else "*")
|
||||
overlay.setVisibility(View.VISIBLE)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +251,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
|
||||
def loadAppsAsync() {
|
||||
Future {
|
||||
ThrowableFuture {
|
||||
while (!appsLoading) loadApps()
|
||||
appsLoading = false
|
||||
handler.post(() => {
|
||||
@@ -316,18 +262,14 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
|
||||
def saveAppSettings(context: Context) {
|
||||
if (apps == null) return
|
||||
val proxiedApps = new StringBuilder
|
||||
apps.foreach(app =>
|
||||
if (app.proxied) {
|
||||
proxiedApps ++= app.uid.toString
|
||||
proxiedApps += '|'
|
||||
})
|
||||
val edit: SharedPreferences.Editor = ShadowsocksApplication.settings.edit
|
||||
edit.putString(Key.proxied, proxiedApps.toString())
|
||||
edit.apply
|
||||
if (!appsLoading) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
|
||||
}
|
||||
|
||||
var handler: Handler = null
|
||||
|
||||
override def onKeyUp(keyCode: Int, event: KeyEvent) = keyCode match {
|
||||
case KeyEvent.KEYCODE_MENU =>
|
||||
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu else toolbar.showOverflowMenu
|
||||
case _ => super.onKeyUp(keyCode, event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{Intent, Context, BroadcastReceiver}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class AppManagerReceiver extends BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent) = if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
|
||||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) AppManager.synchronized(AppManager.cachedApps = null)
|
||||
}
|
||||
@@ -41,16 +41,15 @@ package com.github.shadowsocks
|
||||
|
||||
import java.util.{Timer, TimerTask}
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.os.{Handler, RemoteCallbackList}
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
|
||||
|
||||
trait BaseService {
|
||||
trait BaseService extends Service {
|
||||
|
||||
@volatile private var state = State.INIT
|
||||
@volatile private var state = State.STOPPED
|
||||
@volatile private var callbackCount = 0
|
||||
|
||||
var timer: Timer = null
|
||||
@@ -59,7 +58,7 @@ trait BaseService {
|
||||
|
||||
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
|
||||
|
||||
protected val binder = new IShadowsocksService.Stub {
|
||||
val binder = new IShadowsocksService.Stub {
|
||||
override def getMode: Int = {
|
||||
getServiceMode
|
||||
}
|
||||
@@ -77,7 +76,7 @@ trait BaseService {
|
||||
timer.cancel()
|
||||
timer = null
|
||||
}
|
||||
if (callbackCount == 0 && state != State.CONNECTING && state != State.CONNECTED) {
|
||||
if (callbackCount == 0 && state == State.STOPPED) {
|
||||
stopBackgroundService()
|
||||
}
|
||||
}
|
||||
@@ -102,13 +101,13 @@ trait BaseService {
|
||||
}
|
||||
|
||||
override def stop() {
|
||||
if (state != State.CONNECTING && state != State.STOPPING) {
|
||||
if (state == State.CONNECTED) {
|
||||
stopRunner()
|
||||
}
|
||||
}
|
||||
|
||||
override def start(config: Config) {
|
||||
if (state != State.CONNECTING && state != State.STOPPING) {
|
||||
if (state == State.STOPPED) {
|
||||
startRunner(config)
|
||||
}
|
||||
}
|
||||
@@ -197,8 +196,4 @@ trait BaseService {
|
||||
state = s
|
||||
})
|
||||
}
|
||||
|
||||
def initSoundVibrateLights(notification: Notification) {
|
||||
notification.sound = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
if (item.id == ShadowsocksApplication.profileId) {
|
||||
text.setChecked(true)
|
||||
selectedItem = this
|
||||
} else {
|
||||
text.setChecked(false)
|
||||
if (selectedItem eq this) selectedItem = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +93,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
private val recycleBin = new ArrayBuffer[(Int, Profile)]
|
||||
private var profiles = new ArrayBuffer[Profile]
|
||||
profiles ++= ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List[Profile]())
|
||||
profiles ++= ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
|
||||
def getItemCount = profiles.length
|
||||
|
||||
@@ -145,7 +148,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
val intent = getParentActivityIntent
|
||||
if (intent == null) finish else navigateUpTo(intent)
|
||||
})
|
||||
toolbar.inflateMenu(R.menu.add_profile_methods)
|
||||
toolbar.inflateMenu(R.menu.profile_manager_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
ShadowsocksApplication.profileManager.setProfileAddedListener(profilesAdapter.add)
|
||||
@@ -169,9 +172,8 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
|
||||
attachService(new IShadowsocksServiceCallback.Stub {
|
||||
def stateChanged(state: Int, msg: String) = () // ignore
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) =
|
||||
if (selectedItem != null) selectedItem.updateText(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import com.github.shadowsocks.utils.Action
|
||||
* @author Mygod
|
||||
*/
|
||||
trait ServiceBoundContext extends Context {
|
||||
val connection = new ServiceConnection {
|
||||
|
||||
class ShadowsocksServiceConnection extends ServiceConnection {
|
||||
override def onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
bgService = IShadowsocksService.Stub.asInterface(service)
|
||||
if (callback != null) try {
|
||||
@@ -37,6 +38,7 @@ trait ServiceBoundContext extends Context {
|
||||
def onServiceDisconnected() = ()
|
||||
|
||||
private var callback: IShadowsocksServiceCallback.Stub = _
|
||||
private var connection: ShadowsocksServiceConnection = _
|
||||
|
||||
// Variables
|
||||
var bgService: IShadowsocksService = _
|
||||
@@ -46,9 +48,13 @@ trait ServiceBoundContext extends Context {
|
||||
if (bgService == null) {
|
||||
val s =
|
||||
if (ShadowsocksApplication.isVpnEnabled) classOf[ShadowsocksVpnService] else classOf[ShadowsocksNatService]
|
||||
|
||||
val intent = new Intent(this, s)
|
||||
intent.setAction(Action.SERVICE)
|
||||
|
||||
connection = new ShadowsocksServiceConnection()
|
||||
bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
|
||||
startService(new Intent(this, s))
|
||||
}
|
||||
}
|
||||
@@ -61,8 +67,12 @@ trait ServiceBoundContext extends Context {
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
callback = null
|
||||
}
|
||||
if (connection != null) {
|
||||
unbindService(connection)
|
||||
connection = null
|
||||
}
|
||||
unbindService(connection)
|
||||
bgService = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@ import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
object Typefaces {
|
||||
def get(c: Context, assetPath: String): Typeface = {
|
||||
@@ -150,13 +148,66 @@ class Shadowsocks
|
||||
var fabProgressCircle: FABProgressCircle = _
|
||||
var progressDialog: ProgressDialog = _
|
||||
var progressTag = -1
|
||||
var state = State.INIT
|
||||
var state = State.STOPPED
|
||||
var prepared = false
|
||||
var currentProfile = new Profile
|
||||
var vpnEnabled = -1
|
||||
|
||||
// Services
|
||||
var currentServiceName = classOf[ShadowsocksNatService].getName
|
||||
private val callback = new IShadowsocksServiceCallback.Stub {
|
||||
def stateChanged(s: Int, m: String) {
|
||||
handler.post(() => if (state != s) {
|
||||
s match {
|
||||
case State.CONNECTING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
setPreferenceEnabled(enabled = false)
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
if (state == State.CONNECTING) {
|
||||
fabProgressCircle.beginFinalAnimation()
|
||||
} else {
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
}
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = true)
|
||||
setPreferenceEnabled(enabled = false)
|
||||
case State.STOPPED =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = false)
|
||||
if (m != null) {
|
||||
val snackbar = Snackbar.make(findViewById(android.R.id.content),
|
||||
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
|
||||
if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn,
|
||||
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
|
||||
snackbar.show
|
||||
}
|
||||
setPreferenceEnabled(enabled = true)
|
||||
case State.STOPPING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setEnabled(false)
|
||||
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
|
||||
setPreferenceEnabled(enabled = false)
|
||||
}
|
||||
state = s
|
||||
})
|
||||
}
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
val trafficStat = getString(R.string.stat_summary)
|
||||
.formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal)
|
||||
handler.post(() => {
|
||||
preferences.findPreference(Key.stat).setSummary(trafficStat)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def attachService: Unit = attachService(callback)
|
||||
|
||||
override def onServiceConnected() {
|
||||
// Update the UI
|
||||
@@ -165,6 +216,24 @@ class Shadowsocks
|
||||
|
||||
if (!ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)) {
|
||||
ShadowsocksApplication.settings.edit.putBoolean(ShadowsocksApplication.getVersionName, true).apply()
|
||||
try {
|
||||
// Workaround that convert port(String) to port(Int)
|
||||
val oldLocalPort = ShadowsocksApplication.settings.getString(Key.localPort, "")
|
||||
val oldRemotePort = ShadowsocksApplication.settings.getString(Key.remotePort, "")
|
||||
|
||||
if (oldLocalPort != "") {
|
||||
ShadowsocksApplication.settings.edit.putInt(Key.localPort, oldLocalPort.toInt).apply()
|
||||
}
|
||||
if (oldRemotePort != "") {
|
||||
ShadowsocksApplication.settings.edit.putInt(Key.remotePort, oldRemotePort.toInt).apply()
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception => // Ignore
|
||||
}
|
||||
val oldProxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "")
|
||||
if (oldProxiedApps.contains('|')) ShadowsocksApplication.settings.edit
|
||||
.putString(Key.proxied, DBHelper.updateProxiedApps(this, oldProxiedApps)).apply()
|
||||
|
||||
recovery()
|
||||
}
|
||||
}
|
||||
@@ -173,19 +242,11 @@ class Shadowsocks
|
||||
if (fab != null) fab.setEnabled(false)
|
||||
}
|
||||
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
val trafficStat = getString(R.string.stat_summary)
|
||||
.formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal)
|
||||
handler.post(() => {
|
||||
preferences.findPreference(Key.stat).setSummary(trafficStat)
|
||||
})
|
||||
}
|
||||
|
||||
private lazy val preferences =
|
||||
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
|
||||
private var adView: AdView = _
|
||||
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700)
|
||||
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
|
||||
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_a700)
|
||||
|
||||
var handler = new Handler()
|
||||
|
||||
@@ -262,10 +323,10 @@ class Shadowsocks
|
||||
cmd.append("chmod 666 %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
}
|
||||
|
||||
if (!ShadowsocksApplication.isVpnEnabled) {
|
||||
Console.runRootCommand(cmd.toArray)
|
||||
} else {
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
Console.runCommand(cmd.toArray)
|
||||
} else {
|
||||
Console.runRootCommand(cmd.toArray)
|
||||
}
|
||||
|
||||
cmd.clear()
|
||||
@@ -286,8 +347,7 @@ class Shadowsocks
|
||||
cmd.append("rm -f %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("rm -f %s%s-vpn.conf".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
}
|
||||
Console.runCommand(cmd.toArray)
|
||||
if (!ShadowsocksApplication.isVpnEnabled) {
|
||||
if (ShadowsocksApplication.isVpnEnabled) Console.runCommand(cmd.toArray) else {
|
||||
Console.runRootCommand(cmd.toArray)
|
||||
Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
|
||||
}
|
||||
@@ -306,7 +366,7 @@ class Shadowsocks
|
||||
}
|
||||
|
||||
def prepareStartService() {
|
||||
Future {
|
||||
ThrowableFuture {
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent != null) {
|
||||
@@ -331,7 +391,7 @@ class Shadowsocks
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.layout_main)
|
||||
// Initialize action bar
|
||||
// Initialize Toolbar
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(getString(R.string.screen_name))
|
||||
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
|
||||
@@ -362,16 +422,7 @@ class Shadowsocks
|
||||
})
|
||||
|
||||
// Bind to the service
|
||||
handler.post(() => {
|
||||
attachService(new IShadowsocksServiceCallback.Stub {
|
||||
override def stateChanged(state: Int, msg: String) {
|
||||
onStateChanged(state, msg)
|
||||
}
|
||||
override def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
Shadowsocks.this.trafficUpdated(txRate, rxRate, txTotal, rxTotal)
|
||||
}
|
||||
})
|
||||
})
|
||||
handler.post(() => attachService)
|
||||
}
|
||||
|
||||
def reloadProfile() {
|
||||
@@ -399,26 +450,28 @@ class Shadowsocks
|
||||
bgService.getState match {
|
||||
case State.CONNECTING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
changeSwitch(checked = true)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
setPreferenceEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
changeSwitch(checked = true)
|
||||
serviceStarted = true
|
||||
fab.setImageResource(R.drawable.ic_cloud)
|
||||
setPreferenceEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
|
||||
case State.STOPPING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
changeSwitch(checked = false)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
setPreferenceEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
case _ =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
changeSwitch(checked = false)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_off)
|
||||
setPreferenceEnabled(true)
|
||||
fabProgressCircle.show()
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
|
||||
}
|
||||
state = bgService.getState
|
||||
}
|
||||
@@ -432,7 +485,7 @@ class Shadowsocks
|
||||
// Check if current profile changed
|
||||
if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile()
|
||||
|
||||
trafficUpdated(TrafficMonitor.getTxRate, TrafficMonitor.getRxRate,
|
||||
callback.trafficUpdated(TrafficMonitor.getTxRate, TrafficMonitor.getRxRate,
|
||||
TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal)
|
||||
}
|
||||
|
||||
@@ -457,7 +510,8 @@ class Shadowsocks
|
||||
}
|
||||
|
||||
private def updatePreferenceScreen() {
|
||||
if (ShadowsocksApplication.proxy == "198.199.101.152" && adView == null) {
|
||||
val profile = currentProfile
|
||||
if (profile.host == "198.199.101.152" && adView == null) {
|
||||
adView = new AdView(this)
|
||||
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
|
||||
adView.setAdSize(AdSize.SMART_BANNER)
|
||||
@@ -465,7 +519,6 @@ class Shadowsocks
|
||||
adView.loadAd(new AdRequest.Builder().build())
|
||||
}
|
||||
|
||||
val profile = currentProfile
|
||||
for (name <- Shadowsocks.PROXY_PREFS) {
|
||||
val pref = preferences.findPreference(name)
|
||||
Shadowsocks.updatePreference(pref, name, profile)
|
||||
@@ -488,18 +541,6 @@ class Shadowsocks
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
def copyToSystem() {
|
||||
val ab = new ArrayBuffer[String]
|
||||
ab.append("mount -o rw,remount -t yaffs2 /dev/block/mtdblock3 /system")
|
||||
for (executable <- Shadowsocks.EXECUTABLES) {
|
||||
ab.append("cp %s%s /system/bin/".formatLocal(Locale.ENGLISH, Path.BASE, executable))
|
||||
ab.append("chmod 755 /system/bin/" + executable)
|
||||
ab.append("chown root:shell /system/bin/" + executable)
|
||||
}
|
||||
ab.append("mount -o ro,remount -t yaffs2 /dev/block/mtdblock3 /system")
|
||||
Console.runRootCommand(ab.toArray)
|
||||
}
|
||||
|
||||
def install() {
|
||||
copyAssets(System.getABI)
|
||||
|
||||
@@ -519,7 +560,7 @@ class Shadowsocks
|
||||
def recovery() {
|
||||
serviceStop()
|
||||
val h = showProgress(R.string.recovering)
|
||||
Future {
|
||||
ThrowableFuture {
|
||||
reset()
|
||||
h.sendEmptyMessage(0)
|
||||
}
|
||||
@@ -527,8 +568,9 @@ class Shadowsocks
|
||||
|
||||
def flushDnsCache() {
|
||||
val h = showProgress(R.string.flushing)
|
||||
Future {
|
||||
Utils.toggleAirplaneMode(getBaseContext)
|
||||
ThrowableFuture {
|
||||
if (!Utils.toggleAirplaneMode(getBaseContext)) h.post(() => Snackbar.make(findViewById(android.R.id.content),
|
||||
R.string.flush_dnscache_no_root, Snackbar.LENGTH_LONG).show)
|
||||
h.sendEmptyMessage(0)
|
||||
}
|
||||
}
|
||||
@@ -549,7 +591,7 @@ class Shadowsocks
|
||||
def checkText(key: String): Boolean = {
|
||||
val text = ShadowsocksApplication.settings.getString(key, "")
|
||||
if (text != null && text.length > 0) return true
|
||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.proxy_empty), Snackbar.LENGTH_LONG).show
|
||||
Snackbar.make(findViewById(android.R.id.content), R.string.proxy_empty, Snackbar.LENGTH_LONG).show
|
||||
false
|
||||
}
|
||||
|
||||
@@ -569,42 +611,4 @@ class Shadowsocks
|
||||
progressTag = -1
|
||||
}
|
||||
}
|
||||
|
||||
def onStateChanged(s: Int, m: String) {
|
||||
handler.post(() => if (state != s) {
|
||||
s match {
|
||||
case State.CONNECTING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
setPreferenceEnabled(enabled = false)
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
if (state == State.CONNECTING) {
|
||||
fabProgressCircle.beginFinalAnimation()
|
||||
} else {
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
}
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = true)
|
||||
setPreferenceEnabled(enabled = false)
|
||||
case State.STOPPED =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = false)
|
||||
if (m != null) Snackbar.make(findViewById(android.R.id.content),
|
||||
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show
|
||||
setPreferenceEnabled(enabled = true)
|
||||
case State.STOPPING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setEnabled(false)
|
||||
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
|
||||
setPreferenceEnabled(enabled = false)
|
||||
}
|
||||
state = s
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.preference.PreferenceManager
|
||||
import com.github.shadowsocks.database.{DBHelper, ProfileManager}
|
||||
import com.github.shadowsocks.utils.{Console, Key, Utils}
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders}
|
||||
import com.google.android.gms.common.api.ResultCallback
|
||||
import com.google.android.gms.tagmanager.{ContainerHolder, TagManager}
|
||||
@@ -60,8 +60,7 @@ object ShadowsocksApplication {
|
||||
lazy val settings = PreferenceManager.getDefaultSharedPreferences(instance)
|
||||
lazy val profileManager = new ProfileManager(settings, dbHelper)
|
||||
|
||||
val isRoot = Console.isRoot
|
||||
def isVpnEnabled = !(isRoot && settings.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
|
||||
def isVpnEnabled = !settings.getBoolean(Key.isNAT, false)
|
||||
|
||||
def getVersionName = try {
|
||||
instance.getPackageManager.getPackageInfo(instance.getPackageName, 0).versionName
|
||||
@@ -78,7 +77,6 @@ object ShadowsocksApplication {
|
||||
|
||||
def profileId = settings.getInt(Key.profileId, -1)
|
||||
def profileId(i: Int) = settings.edit.putInt(Key.profileId, i).apply
|
||||
def proxy = settings.getString(Key.proxy, "")
|
||||
def currentProfile = profileManager.getProfile(profileId)
|
||||
|
||||
def switchProfile(id: Int) = {
|
||||
|
||||
@@ -44,25 +44,19 @@ import java.lang.Process
|
||||
import java.net.{Inet6Address, InetAddress}
|
||||
import java.util.Locale
|
||||
|
||||
import android.app._
|
||||
import android.content._
|
||||
import android.content.pm.{PackageInfo, PackageManager}
|
||||
import android.net.{ConnectivityManager, Network}
|
||||
import android.os._
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.{Log, SparseArray}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.aidl.Config
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection._
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ShadowsocksNatService extends Service with BaseService {
|
||||
class ShadowsocksNatService extends BaseService {
|
||||
|
||||
val TAG = "ShadowsocksNatService"
|
||||
|
||||
@@ -70,16 +64,15 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
val CMD_IPTABLES_DNAT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " +
|
||||
"-j DNAT --to-destination 127.0.0.1:8123"
|
||||
|
||||
var lockReceiver: BroadcastReceiver = null
|
||||
var closeReceiver: BroadcastReceiver = null
|
||||
var connReceiver: BroadcastReceiver = null
|
||||
var apps: Array[ProxiedApp] = null
|
||||
private var notification: ShadowsocksNotification = _
|
||||
var closeReceiver: BroadcastReceiver = _
|
||||
var connReceiver: BroadcastReceiver = _
|
||||
val myUid = android.os.Process.myUid()
|
||||
|
||||
var sslocalProcess: Process = null
|
||||
var sstunnelProcess: Process = null
|
||||
var redsocksProcess: Process = null
|
||||
var pdnsdProcess: Process = null
|
||||
var sslocalProcess: Process = _
|
||||
var sstunnelProcess: Process = _
|
||||
var redsocksProcess: Process = _
|
||||
var pdnsdProcess: Process = _
|
||||
|
||||
private val dnsAddressCache = new SparseArray[String]
|
||||
|
||||
@@ -149,7 +142,7 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
})
|
||||
Console.runRootCommand(cmdBuf.toArray)
|
||||
} else {
|
||||
Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
|
||||
Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,33 +317,6 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
true
|
||||
}
|
||||
|
||||
def notifyForegroundAlert(title: String, info: String, visible: Boolean) {
|
||||
val openIntent = new Intent(this, classOf[Shadowsocks])
|
||||
openIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
val contentIntent = PendingIntent.getActivity(this, 0, openIntent, 0)
|
||||
val closeIntent = new Intent(Action.CLOSE)
|
||||
val actionIntent = PendingIntent.getBroadcast(this, 0, closeIntent, 0)
|
||||
val builder = new NotificationCompat.Builder(this)
|
||||
|
||||
builder
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(this, R.color.material_accent_500))
|
||||
.setTicker(title)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(info)
|
||||
.setContentIntent(contentIntent)
|
||||
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop),
|
||||
actionIntent)
|
||||
|
||||
if (visible)
|
||||
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
else
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
|
||||
startForeground(1, builder.build)
|
||||
}
|
||||
|
||||
def onBind(intent: Intent): IBinder = {
|
||||
Log.d(TAG, "onBind")
|
||||
if (Action.SERVICE == intent.getAction) {
|
||||
@@ -408,29 +374,29 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
http_sb.append(Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS)
|
||||
}
|
||||
if (config.isProxyApps) {
|
||||
if (apps == null || apps.length <= 0) {
|
||||
apps = AppManager.getProxiedApps(this, config.proxiedAppString)
|
||||
}
|
||||
val uidSet: mutable.HashSet[Int] = new mutable.HashSet[Int]
|
||||
for (app <- apps) {
|
||||
if (app.proxied) {
|
||||
uidSet.add(app.uid)
|
||||
}
|
||||
}
|
||||
for (uid <- uidSet) {
|
||||
if (!config.isBypassApps) {
|
||||
http_sb.append((Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS).replace("-t nat", "-t nat -m owner --uid-owner " + uid))
|
||||
} else {
|
||||
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid))
|
||||
}
|
||||
val uidMap = getPackageManager.getInstalledApplications(0).map(ai => ai.packageName -> ai.uid).toMap
|
||||
for (pn <- config.proxiedAppString.split('\n')) uidMap.get(pn) match {
|
||||
case Some(uid) =>
|
||||
if (!config.isBypassApps) {
|
||||
http_sb.append((Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS)
|
||||
.replace("-t nat", "-t nat -m owner --uid-owner " + uid))
|
||||
} else {
|
||||
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid))
|
||||
}
|
||||
case _ => // probably removed package, ignore
|
||||
}
|
||||
}
|
||||
Console.runRootCommand(init_sb.toArray)
|
||||
Console.runRootCommand(http_sb.toArray)
|
||||
Console.runRootCommand((init_sb ++ http_sb).toArray)
|
||||
}
|
||||
|
||||
override def startRunner(config: Config) {
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
if (!Console.isRoot) {
|
||||
changeState(State.STOPPED, getString(R.string.nat_no_root))
|
||||
return
|
||||
}
|
||||
|
||||
super.startRunner(config)
|
||||
|
||||
// register close receiver
|
||||
@@ -443,36 +409,9 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
}
|
||||
registerReceiver(closeReceiver, filter)
|
||||
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
val screenFilter = new IntentFilter()
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
screenFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
lockReceiver = (context: Context, intent: Intent) => if (getState == State.CONNECTED) {
|
||||
val action = intent.getAction
|
||||
if (action == Intent.ACTION_SCREEN_OFF) {
|
||||
notifyForegroundAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), false)
|
||||
} else if (action == Intent.ACTION_SCREEN_ON) {
|
||||
val keyGuard = getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||
if (!keyGuard.inKeyguardRestrictedInputMode) {
|
||||
notifyForegroundAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
|
||||
}
|
||||
} else if (action == Intent.ACTION_USER_PRESENT) {
|
||||
notifyForegroundAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
|
||||
}
|
||||
}
|
||||
registerReceiver(lockReceiver, screenFilter)
|
||||
}
|
||||
|
||||
ShadowsocksApplication.track(TAG, "start")
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
|
||||
Future {
|
||||
|
||||
ThrowableFuture {
|
||||
if (config.proxy == "198.199.101.152") {
|
||||
val holder = ShadowsocksApplication.containerHolder
|
||||
try {
|
||||
@@ -507,9 +446,8 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
// Set DNS
|
||||
flushDns()
|
||||
|
||||
notifyForegroundAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
|
||||
changeState(State.CONNECTED)
|
||||
notification = new ShadowsocksNotification(this, config.profileName, true)
|
||||
} else {
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
@@ -531,12 +469,7 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
closeReceiver = null
|
||||
}
|
||||
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
if (lockReceiver != null) {
|
||||
unregisterReceiver(lockReceiver)
|
||||
lockReceiver = null
|
||||
}
|
||||
}
|
||||
if (notification != null) notification.destroy()
|
||||
|
||||
ShadowsocksApplication.track(TAG, "stop")
|
||||
|
||||
@@ -548,8 +481,6 @@ class ShadowsocksNatService extends Service with BaseService {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
stopForeground(true)
|
||||
|
||||
// change the state
|
||||
changeState(State.STOPPED)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.app.{KeyguardManager, PendingIntent}
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
||||
import android.support.v4.content.ContextCompat
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
|
||||
import com.github.shadowsocks.utils.{Action, State, Utils}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
|
||||
private lazy val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||
private lazy val callback = new Stub {
|
||||
override def stateChanged(state: Int, msg: String) = () // ignore
|
||||
override def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txRate, rxRate))
|
||||
style.bigText(service.getString(R.string.stat_summary)
|
||||
.formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal))
|
||||
show()
|
||||
}
|
||||
}
|
||||
private var lockReceiver: BroadcastReceiver = _
|
||||
private var callbackRegistered: Boolean = _
|
||||
|
||||
private val builder = new NotificationCompat.Builder(service)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_accent_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks])
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
|
||||
.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))
|
||||
private lazy val style = new BigTextStyle(builder)
|
||||
private val showOnUnlock = visible && Utils.isLollipopOrAbove
|
||||
private var isVisible = true
|
||||
update(if (service.getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager].isScreenOn)
|
||||
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
lockReceiver = (context: Context, intent: Intent) => update(intent.getAction)
|
||||
val screenFilter = new IntentFilter()
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
if (showOnUnlock) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(lockReceiver, screenFilter)
|
||||
|
||||
private def update(action: String, forceShow: Boolean = false) =
|
||||
if (forceShow || service.getState == State.CONNECTED) action match {
|
||||
case Intent.ACTION_SCREEN_OFF =>
|
||||
setVisible(false, forceShow)
|
||||
unregisterCallback // unregister callback to save battery
|
||||
case Intent.ACTION_SCREEN_ON =>
|
||||
setVisible(showOnUnlock && !keyGuard.inKeyguardRestrictedInputMode, forceShow)
|
||||
service.binder.registerCallback(callback)
|
||||
callbackRegistered = true
|
||||
case Intent.ACTION_USER_PRESENT => setVisible(showOnUnlock, forceShow)
|
||||
}
|
||||
|
||||
private def unregisterCallback = if (callbackRegistered) {
|
||||
service.binder.unregisterCallback(callback)
|
||||
callbackRegistered = false
|
||||
}
|
||||
|
||||
def setVisible(visible: Boolean, forceShow: Boolean = false) = if (isVisible != visible) {
|
||||
isVisible = visible
|
||||
builder.setPriority(if (visible) NotificationCompat.PRIORITY_DEFAULT else NotificationCompat.PRIORITY_MIN)
|
||||
show()
|
||||
} else if (forceShow) show()
|
||||
|
||||
def notification = builder.build()
|
||||
def show() = service.startForeground(1, notification)
|
||||
|
||||
def destroy() {
|
||||
if (lockReceiver != null) {
|
||||
service.unregisterReceiver(lockReceiver)
|
||||
lockReceiver = null
|
||||
}
|
||||
unregisterCallback
|
||||
service.stopForeground(true)
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,9 @@ import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
class ShadowsocksReceiver extends BroadcastReceiver {
|
||||
|
||||
val TAG = "Shadowsocks"
|
||||
|
||||
def onReceive(context: Context, intent: Intent) {
|
||||
val isAutoConnect: Boolean = ShadowsocksApplication.settings.getBoolean(Key.isAutoConnect, false)
|
||||
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
|
||||
if (isAutoConnect && isInstalled) {
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
} else {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerActivity])
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
if (ShadowsocksApplication.settings.getBoolean(Key.isAutoConnect, false)) {
|
||||
Utils.startSsService(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.github.shadowsocks
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.content.{DialogInterface, Intent}
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.{DialogInterface, Intent, SharedPreferences}
|
||||
import android.net.Uri
|
||||
import android.os.{Build, Bundle}
|
||||
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
|
||||
@@ -11,14 +12,16 @@ import android.webkit.{WebView, WebViewClient}
|
||||
import com.github.shadowsocks.utils.Key
|
||||
|
||||
// TODO: Move related logic here
|
||||
class ShadowsocksSettings extends PreferenceFragment {
|
||||
private lazy val activity = getActivity.asInstanceOf[Shadowsocks]
|
||||
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
|
||||
private def activity = getActivity.asInstanceOf[Shadowsocks]
|
||||
lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
|
||||
|
||||
private var isProxyApps: SwitchPreference = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
addPreferencesFromResource(R.xml.pref_all)
|
||||
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference]
|
||||
isProxyApps.setOnPreferenceChangeListener((preference: Preference, newValue: Any) => {
|
||||
@@ -26,12 +29,6 @@ class ShadowsocksSettings extends PreferenceFragment {
|
||||
newValue.asInstanceOf[Boolean] // keep it ON
|
||||
})
|
||||
|
||||
findPreference(Key.isNAT).setOnPreferenceChangeListener((preference: Preference, newValue: Any) =>
|
||||
if (ShadowsocksApplication.isRoot) activity.handler.post(() => {
|
||||
activity.deattachService()
|
||||
activity.attachService()
|
||||
}) else false)
|
||||
|
||||
findPreference("recovery").setOnPreferenceClickListener((preference: Preference) => {
|
||||
ShadowsocksApplication.track(Shadowsocks.TAG, "reset")
|
||||
activity.recovery()
|
||||
@@ -39,10 +36,8 @@ class ShadowsocksSettings extends PreferenceFragment {
|
||||
})
|
||||
|
||||
val flush = findPreference("flush_dnscache")
|
||||
if (Build.VERSION.SDK_INT >= 17 && !ShadowsocksApplication.isRoot) {
|
||||
flush.setEnabled(false)
|
||||
flush.setSummary(R.string.flush_dnscache_summary_disabled)
|
||||
} else flush.setOnPreferenceClickListener((preference: Preference) => {
|
||||
if (Build.VERSION.SDK_INT < 17) flush.setSummary(R.string.flush_dnscache_summary)
|
||||
flush.setOnPreferenceClickListener(_ => {
|
||||
ShadowsocksApplication.track(Shadowsocks.TAG, "flush_dnscache")
|
||||
activity.flushDnsCache()
|
||||
true
|
||||
@@ -71,8 +66,16 @@ class ShadowsocksSettings extends PreferenceFragment {
|
||||
})
|
||||
}
|
||||
|
||||
override def onResume = {
|
||||
override def onResume {
|
||||
super.onResume
|
||||
isProxyApps.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isProxyApps, false)) // update
|
||||
}
|
||||
|
||||
def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = key match {
|
||||
case Key.isNAT => activity.handler.post(() => {
|
||||
activity.deattachService
|
||||
activity.attachService
|
||||
})
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,10 @@ import java.io.File
|
||||
import java.lang.Process
|
||||
import java.util.Locale
|
||||
|
||||
import android.app._
|
||||
import android.content._
|
||||
import android.content.pm.{PackageInfo, PackageManager}
|
||||
import android.net.VpnService
|
||||
import android.os._
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.aidl.Config
|
||||
@@ -57,25 +54,22 @@ import com.github.shadowsocks.utils._
|
||||
import org.apache.commons.net.util.SubnetUtils
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
val TAG = "ShadowsocksVpnService"
|
||||
val VPN_MTU = 1500
|
||||
val PRIVATE_VLAN = "26.26.26.%s"
|
||||
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
|
||||
var conn: ParcelFileDescriptor = null
|
||||
var apps: Array[ProxiedApp] = null
|
||||
var vpnThread: ShadowsocksVpnThread = null
|
||||
var closeReceiver: BroadcastReceiver = null
|
||||
var conn: ParcelFileDescriptor = _
|
||||
var vpnThread: ShadowsocksVpnThread = _
|
||||
private var notification: ShadowsocksNotification = _
|
||||
var closeReceiver: BroadcastReceiver = _
|
||||
|
||||
var sslocalProcess: Process = null
|
||||
var sstunnelProcess: Process = null
|
||||
var pdnsdProcess: Process = null
|
||||
var tun2socksProcess: Process = null
|
||||
var sslocalProcess: Process = _
|
||||
var sstunnelProcess: Process = _
|
||||
var pdnsdProcess: Process = _
|
||||
var tun2socksProcess: Process = _
|
||||
|
||||
def isByass(net: SubnetUtils): Boolean = {
|
||||
val info = net.getInfo
|
||||
@@ -108,32 +102,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
null
|
||||
}
|
||||
|
||||
def notifyForegroundAlert(title: String, info: String, visible: Boolean) {
|
||||
val openIntent = new Intent(this, classOf[Shadowsocks])
|
||||
val contentIntent = PendingIntent.getActivity(this, 0, openIntent, 0)
|
||||
val closeIntent = new Intent(Action.CLOSE)
|
||||
val actionIntent = PendingIntent.getBroadcast(this, 0, closeIntent, 0)
|
||||
val builder = new NotificationCompat.Builder(this)
|
||||
|
||||
builder
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(this, R.color.material_accent_500))
|
||||
.setTicker(title)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(info)
|
||||
.setContentIntent(contentIntent)
|
||||
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop),
|
||||
actionIntent)
|
||||
|
||||
if (visible)
|
||||
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
else
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
|
||||
startForeground(1, builder.build)
|
||||
}
|
||||
|
||||
override def onCreate() {
|
||||
super.onCreate()
|
||||
ConfigUtils.refresh(this)
|
||||
@@ -152,7 +120,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
vpnThread = null
|
||||
}
|
||||
|
||||
stopForeground(true)
|
||||
if (notification != null) notification.destroy()
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPING)
|
||||
@@ -243,7 +211,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
|
||||
Future {
|
||||
ThrowableFuture {
|
||||
if (config.proxy == "198.199.101.152") {
|
||||
val holder = ShadowsocksApplication.containerHolder
|
||||
try {
|
||||
@@ -275,9 +243,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
if (resolved && handleConnection) {
|
||||
notifyForegroundAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), false)
|
||||
changeState(State.CONNECTED)
|
||||
notification = new ShadowsocksNotification(this, config.profileName)
|
||||
} else {
|
||||
changeState(State.STOPPED, getString(R.string.service_failed))
|
||||
stopRunner()
|
||||
@@ -421,14 +388,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
|
||||
if (config.isProxyApps) {
|
||||
val apps = AppManager.getProxiedApps(this, config.proxiedAppString)
|
||||
val pkgSet: mutable.HashSet[String] = new mutable.HashSet[String]
|
||||
for (app <- apps) {
|
||||
if (app.proxied) {
|
||||
pkgSet.add(app.packageName)
|
||||
}
|
||||
}
|
||||
for (pkg <- pkgSet) {
|
||||
for (pkg <- config.proxiedAppString.split('\n')) {
|
||||
if (!config.isBypassApps) {
|
||||
builder.addAllowedApplication(pkg)
|
||||
} else {
|
||||
@@ -478,7 +438,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
+ "--loglevel 3")
|
||||
.formatLocal(Locale.ENGLISH,
|
||||
PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"),
|
||||
config.localPort, fd, VPN_MTU, Path.BASE)
|
||||
config.localPort, fd, VPN_MTU)
|
||||
|
||||
if (config.isIpv6)
|
||||
cmd += " --netif-ip6addr " + PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2014 <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 android.app.Activity
|
||||
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.{Switch, CheckedTextView}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
|
||||
/**
|
||||
* @author CzBiX
|
||||
*/
|
||||
class TaskerActivity 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 bindDefault {
|
||||
item = null
|
||||
text.setText(R.string.profile_default)
|
||||
text.setChecked(taskerOption.profileId < 0)
|
||||
}
|
||||
def bind(item: Profile) {
|
||||
this.item = item
|
||||
text.setText(item.name)
|
||||
text.setChecked(taskerOption.profileId == item.id)
|
||||
}
|
||||
|
||||
def onClick(v: View) {
|
||||
taskerOption.switchOn = switch.isChecked
|
||||
taskerOption.profileId = if (item == null) -1 else item.id
|
||||
setResult(Activity.RESULT_OK, taskerOption.toIntent(TaskerActivity.this))
|
||||
finish
|
||||
}
|
||||
}
|
||||
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
private val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
def getItemCount = 1 + profiles.length
|
||||
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = i match {
|
||||
case 0 => vh.bindDefault
|
||||
case _ => vh.bind(profiles(i - 1))
|
||||
}
|
||||
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 var taskerOption: TaskerSettings = _
|
||||
private var switch: Switch = _
|
||||
private val profilesAdapter = new ProfilesAdapter
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.layout_tasker)
|
||||
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(R.string.app_name)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close)
|
||||
toolbar.setNavigationOnClickListener(_ => finish())
|
||||
|
||||
taskerOption = TaskerSettings.fromIntent(getIntent)
|
||||
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
|
||||
switch.setChecked(taskerOption.switchOn)
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
profilesList.setLayoutManager(new LinearLayoutManager(this))
|
||||
profilesList.setItemAnimator(new DefaultItemAnimator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2014 <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 android.content.{BroadcastReceiver, Context, Intent}
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
/**
|
||||
* @author CzBiX
|
||||
*/
|
||||
class TaskerReceiver extends BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent) {
|
||||
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
|
||||
}
|
||||
if (settings.switchOn) Utils.startSsService(context) else if (!switched) Utils.stopSsService(context)
|
||||
}
|
||||
}
|
||||
@@ -40,18 +40,32 @@
|
||||
package com.github.shadowsocks.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
import com.j256.ormlite.table.TableUtils
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable
|
||||
|
||||
object DBHelper {
|
||||
val PROFILE = "profile.db"
|
||||
final val PROFILE = "profile.db"
|
||||
private var apps: mutable.Buffer[ApplicationInfo] = _
|
||||
|
||||
def isAllDigits(x: String) = !x.isEmpty && (x forall Character.isDigit)
|
||||
|
||||
def updateProxiedApps(context: Context, old: String) = {
|
||||
synchronized(if (apps == null) apps = context.getPackageManager.getInstalledApplications(0).asScala)
|
||||
val uidSet = old.split('|').filter(isAllDigits).map(_.toInt).toSet
|
||||
apps.filter(ai => uidSet.contains(ai.uid)).map(_.packageName).mkString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
class DBHelper(val context: Context)
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 13) {
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 14) {
|
||||
import DBHelper._
|
||||
|
||||
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
|
||||
|
||||
@@ -81,20 +95,26 @@ class DBHelper(val context: Context)
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
|
||||
onCreate(database, connectionSource)
|
||||
TableUtils.createTable(connectionSource, classOf[Profile])
|
||||
profileDao.executeRawNoArgs(
|
||||
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
|
||||
" udpdns, auth, ipv6, individual) " +
|
||||
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, auth," +
|
||||
" ipv6, individual FROM `tmp`;")
|
||||
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
|
||||
return
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
} else if (oldVersion < 13) {
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;")
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date DATE;")
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
|
||||
}
|
||||
if (oldVersion < 14) {
|
||||
for (profile <- profileDao.queryForAll.asScala) {
|
||||
profile.individual = updateProxiedApps(context, profile.individual)
|
||||
profileDao.update(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
|
||||
profile.route = oldProfile.route
|
||||
profile.ipv6 = oldProfile.ipv6
|
||||
profile.proxyApps = oldProfile.proxyApps
|
||||
profile.bypass = oldProfile.bypass
|
||||
profile.individual = oldProfile.individual
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
case _ =>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2014 <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.helper
|
||||
|
||||
import android.content.{Context, Intent}
|
||||
import android.os.Bundle
|
||||
import com.github.shadowsocks.{R, ShadowsocksApplication}
|
||||
import com.twofortyfouram.locale.api.{Intent => ApiIntent}
|
||||
|
||||
object TaskerSettings {
|
||||
private val KEY_SWITCH_ON = "switch_on"
|
||||
private val KEY_PROFILE_ID = "profile_id"
|
||||
|
||||
def fromIntent(intent: Intent) = new TaskerSettings(if (intent.hasExtra(ApiIntent.EXTRA_BUNDLE))
|
||||
intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE) else Bundle.EMPTY)
|
||||
|
||||
def fromBundle(bundle: Bundle) = new TaskerSettings(bundle)
|
||||
}
|
||||
|
||||
class TaskerSettings(bundle: Bundle) {
|
||||
import TaskerSettings._
|
||||
|
||||
var switchOn = bundle.getBoolean(KEY_SWITCH_ON, true)
|
||||
var profileId = bundle.getInt(KEY_PROFILE_ID, -1)
|
||||
|
||||
def toIntent(context: Context) = {
|
||||
val bundle = new Bundle()
|
||||
if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false)
|
||||
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
|
||||
new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
|
||||
ShadowsocksApplication.profileManager.getProfile(profileId) match {
|
||||
case Some(p) => context.getString(if (switchOn) R.string.start_service else R.string.stop_service, p.name)
|
||||
case None => context.getString(if (switchOn) R.string.start_service_default else R.string.stop)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
package object shadowsocks {
|
||||
def ThrowableFuture[T](f: => T) = Future(f) onFailure {
|
||||
case e: Throwable => e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
|
||||
mSpinner.performClick
|
||||
true
|
||||
})
|
||||
initSummary(mContext, attrs)
|
||||
val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference)
|
||||
setEntries(a.getTextArray(R.styleable.DropDownPreference_entries))
|
||||
mEntryValues = a.getTextArray(R.styleable.DropDownPreference_entryValues)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.preference.SwitchPreference
|
||||
import android.util.AttributeSet
|
||||
import com.github.shadowsocks.{R, ShadowsocksApplication}
|
||||
|
||||
/**
|
||||
* TODO: No animations?
|
||||
* @author Mygod
|
||||
*/
|
||||
final class NatSwitchPreference(context: Context, attrs: AttributeSet) extends SwitchPreference(context, attrs) {
|
||||
private var disabled = false
|
||||
|
||||
override def isEnabled = super.isEnabled && !disabled
|
||||
override def setEnabled(b: Boolean) = super.setEnabled(b && !disabled)
|
||||
|
||||
if (!ShadowsocksApplication.isRoot) {
|
||||
disabled = true
|
||||
setEnabled(false)
|
||||
setSummary(R.string.nat_summary_no_root)
|
||||
}
|
||||
|
||||
override def onGetDefaultValue(a: TypedArray, index: Int) =
|
||||
(!ShadowsocksApplication.isVpnEnabled).asInstanceOf[AnyRef]
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Bundle
|
||||
import android.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.view.{ViewGroup, WindowManager}
|
||||
import android.widget.NumberPicker
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
@@ -22,7 +23,6 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
|
||||
setMax(a.getInt(R.styleable.NumberPickerPreference_max, Int.MaxValue - 1))
|
||||
a.recycle
|
||||
}
|
||||
initSummary(context, attrs)
|
||||
|
||||
def getValue = value
|
||||
def getMin = if (picker == null) 0 else picker.getMinValue
|
||||
@@ -37,6 +37,10 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
|
||||
def setMin(value: Int) = picker.setMinValue(value)
|
||||
def setMax(value: Int) = picker.setMaxValue(value)
|
||||
|
||||
override protected def showDialog(state: Bundle) {
|
||||
super.showDialog(state)
|
||||
getDialog.getWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
override protected def onCreateDialogView = {
|
||||
val parent = picker.getParent.asInstanceOf[ViewGroup]
|
||||
if (parent != null) parent.removeView(picker)
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.Preference
|
||||
import android.util.AttributeSet
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* Make your preference support %s in summary. Override getSummaryValue to customize what to put in.
|
||||
* @author Mygod
|
||||
*/
|
||||
trait SummaryPreference extends Preference {
|
||||
private var mSummary: String = _
|
||||
|
||||
protected def initSummary(context: Context, attrs: AttributeSet) {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.SummaryPreference)
|
||||
mSummary = a.getString(R.styleable.SummaryPreference_summary)
|
||||
a.recycle
|
||||
}
|
||||
|
||||
protected def getSummaryValue: AnyRef
|
||||
|
||||
/**
|
||||
@@ -26,19 +15,5 @@ trait SummaryPreference extends Preference {
|
||||
*
|
||||
* @return the summary with appropriate string substitution
|
||||
*/
|
||||
override def getSummary = {
|
||||
val entry = getSummaryValue
|
||||
if (mSummary == null || entry == null) super.getSummary else String.format(mSummary, entry)
|
||||
}
|
||||
/**
|
||||
* Sets the summary for this Preference with a CharSequence. If the summary has a String formatting marker in it
|
||||
* (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place when it's retrieved.
|
||||
*
|
||||
* @param summary The summary for the preference.
|
||||
*/
|
||||
override def setSummary(summary: CharSequence) {
|
||||
super.setSummary(summary)
|
||||
if (summary == null && mSummary != null) mSummary = null
|
||||
else if (summary != null && summary != mSummary) mSummary = summary.toString
|
||||
}
|
||||
override def getSummary = String.format(super.getSummary.toString, getSummaryValue)
|
||||
}
|
||||
|
||||
@@ -75,17 +75,11 @@ object Console {
|
||||
shell.close()
|
||||
}
|
||||
|
||||
def runRootCommand(command: String): String = runRootCommand(Array(command))
|
||||
def runRootCommand(command: String, context: String): String = runRootCommand(Array(command), context)
|
||||
def runRootCommand(commands: Array[String]): String = runRootCommand(commands, "u:r:init_shell:s0")
|
||||
|
||||
def runRootCommand(commands: Array[String], context: String): String = {
|
||||
if (!isRoot) {
|
||||
return null
|
||||
}
|
||||
val shell = openRootShell(context)
|
||||
def runRootCommand(commands: String*): String = runRootCommand(commands.toArray)
|
||||
def runRootCommand(commands: Array[String]): String = {
|
||||
val shell = openRootShell("u:r:init_shell:s0")
|
||||
val sb = new StringBuilder
|
||||
shell.addCommand(commands, 0, ((commandCode: Int, exitCode: Int, output: util.List[String]) => {
|
||||
shell.addCommand(commands, 0, ((_, exitCode, output) => {
|
||||
if (exitCode < 0) {
|
||||
shell.close()
|
||||
} else {
|
||||
|
||||
@@ -82,8 +82,8 @@ object Key {
|
||||
val proxy = "proxy"
|
||||
val sitekey = "sitekey"
|
||||
val encMethod = "encMethod"
|
||||
val remotePort = "remotePort"
|
||||
val localPort = "port"
|
||||
val remotePort = "remotePortNum"
|
||||
val localPort = "localPortNum"
|
||||
}
|
||||
|
||||
object Scheme {
|
||||
@@ -98,7 +98,6 @@ object Mode {
|
||||
}
|
||||
|
||||
object State {
|
||||
val INIT = 0
|
||||
val CONNECTING = 1
|
||||
val CONNECTED = 2
|
||||
val STOPPING = 3
|
||||
|
||||
@@ -27,11 +27,11 @@ object TrafficMonitor {
|
||||
}
|
||||
|
||||
private val units = Array("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
|
||||
private val numberFormat = new DecimalFormat("0.00")
|
||||
private val numberFormat = new DecimalFormat("@@@")
|
||||
def formatTraffic(size: Long): String = {
|
||||
var n: Double = size
|
||||
var i = -1
|
||||
while (n >= 1024) {
|
||||
while (n >= 1000) {
|
||||
n /= 1024
|
||||
i = i + 1
|
||||
}
|
||||
@@ -52,8 +52,8 @@ object TrafficMonitor {
|
||||
}
|
||||
|
||||
def update(tx: Long, rx: Long) {
|
||||
txTotal = tx;
|
||||
rxTotal = rx;
|
||||
txTotal = tx
|
||||
rxTotal = rx
|
||||
}
|
||||
|
||||
def reset() {
|
||||
|
||||
@@ -55,32 +55,18 @@ import android.util.{DisplayMetrics, Base64, Log}
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.{Gravity, View, Window}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.{ShadowsocksApplication, BuildConfig}
|
||||
import com.github.shadowsocks.{ShadowsocksRunnerActivity, ShadowsocksRunnerService, ShadowsocksApplication, BuildConfig}
|
||||
import org.xbill.DNS._
|
||||
|
||||
|
||||
object Utils {
|
||||
|
||||
val TAG: String = "Shadowsocks"
|
||||
val ABI_PROP: String = "ro.product.cpu.abi"
|
||||
val ABI2_PROP: String = "ro.product.cpu.abi2"
|
||||
val ARM_ABI: String = "armeabi"
|
||||
val ARMV7_ABI: String = "armeabi-v7a"
|
||||
val X86_ABI: String = "x86"
|
||||
val MIPS_ABI: String = "mips"
|
||||
val DEFAULT_SHELL: String = "/system/bin/sh"
|
||||
val DEFAULT_ROOT: String = "/system/bin/su"
|
||||
val ALTERNATIVE_ROOT: String = "/system/xbin/su"
|
||||
val DEFAULT_IPTABLES: String = "/system/bin/iptables"
|
||||
val ALTERNATIVE_IPTABLES: String = "iptables"
|
||||
val TIME_OUT: Int = -99
|
||||
var initialized: Boolean = false
|
||||
var hasRedirectSupport: Int = -1
|
||||
var shell: String = null
|
||||
var root_shell: String = null
|
||||
var iptables: String = null
|
||||
var data_path: String = null
|
||||
var rootTries = 0
|
||||
|
||||
def isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
|
||||
@@ -154,12 +140,14 @@ object Utils {
|
||||
}
|
||||
|
||||
// Blocked > 3 seconds
|
||||
def toggleAirplaneMode(context: Context) {
|
||||
if (ShadowsocksApplication.isRoot) {
|
||||
Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
|
||||
def toggleAirplaneMode(context: Context) = {
|
||||
if (Console.isRoot) {
|
||||
Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
|
||||
true
|
||||
} else if (Build.VERSION.SDK_INT < 17) {
|
||||
toggleBelowApiLevel17(context)
|
||||
}
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
//noinspection ScalaDeprecation
|
||||
@@ -319,8 +307,7 @@ object Utils {
|
||||
var compatible: Boolean = false
|
||||
var version: Boolean = false
|
||||
|
||||
val command = Array(iptables + " --version", iptables + " -L -t nat -n")
|
||||
val lines = Console.runRootCommand(command)
|
||||
val lines = Console.runRootCommand(iptables + " --version", iptables + " -L -t nat -n")
|
||||
if (lines == null) return
|
||||
|
||||
if (lines.contains("OUTPUT")) {
|
||||
@@ -397,11 +384,8 @@ object Utils {
|
||||
if (!Console.isRoot) return
|
||||
hasRedirectSupport = 1
|
||||
|
||||
val sb = new StringBuilder
|
||||
val command = Utils.getIptables + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
|
||||
val lines = Console.runRootCommand(command)
|
||||
|
||||
Console.runRootCommand(command.replace("-A", "-D"))
|
||||
val lines = Console.runRootCommand(command, command.replace("-A", "-D"))
|
||||
if (lines == null) return
|
||||
if (lines.contains("No chain/target/match")) {
|
||||
hasRedirectSupport = 0
|
||||
@@ -416,6 +400,25 @@ object Utils {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def startSsService(context: Context): Unit = {
|
||||
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
|
||||
if (!isInstalled) return
|
||||
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
} else {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerActivity])
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
def stopSsService(context: Context): Unit = {
|
||||
context.sendBroadcast(new Intent(Action.CLOSE))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user