Compare commits

...
65 Commits
Author SHA1 Message Date
Max Lv dac51aa112 Update shadowsocks-libev 2015-12-11 20:54:17 +08:00
Max Lv d2f245fd2f Merge pull request #472 from Mygod/master
Fix #471
2015-12-08 23:01:05 +08:00
Mygod 7e6bc0d740 Provide a better workaround for hiding FAB
Fix #471
2015-12-08 22:34:27 +08:00
Mygod de768f7979 Hide apps that doesn't require INTERNET permission 2015-12-08 22:19:02 +08:00
Mygod edd5fb4a4c Use brighter accent color for FAB 2015-12-08 22:08:20 +08:00
Mygod 4d81cacbdd Fix syncing issue 2015-12-08 21:44:19 +08:00
Max Lv 38b333811e Fix a crash when switching NAT mode 2015-12-07 17:25:50 +08:00
Max Lv 63569a4448 fix a crash when upgrading database 2015-12-07 17:15:28 +08:00
Max Lv 1023454266 Merge pull request #469 from Mygod/master
Persist package names in individual/proxied
2015-12-07 10:06:15 +08:00
Mygod 4312e1676d Fix label 2015-12-06 21:38:48 +08:00
Mygod 3f93ad079b Sort packages with case ignored 2015-12-06 21:34:50 +08:00
Mygod ad831aa1f4 Persist package names in individual/proxied 2015-12-06 21:20:30 +08:00
Mygod 93084f0ed0 Ask for ROOT only when it's needed 2015-12-06 20:04:41 +08:00
Max Lv d27407003c Update shadowsocks-libev 2015-12-06 09:59:11 +08:00
Max Lv 9541ac0111 Merge pull request #467 from Mygod/master
Cache proxied apps for even better performance
2015-12-06 09:25:16 +08:00
Mygod ac98caa4b4 Move code 2015-12-06 00:07:01 +08:00
Mygod 8c2690c0fb Fix #468 2015-12-06 00:02:29 +08:00
Mygod c6a7560bae Cache proxied apps for even better performance 2015-12-05 22:07:25 +08:00
Mygod dd4206613f Update android-sdk-plugin 2015-12-05 20:55:01 +08:00
Mygod 348c7b6bd1 Fix menu button in AppManager 2015-12-05 20:53:43 +08:00
Mygod 37d05236be Improve performance when fetching app list 2015-12-05 20:41:55 +08:00
Mygod faa1e5def4 Merge branch 'master' of https://github.com/shadowsocks/shadowsocks-android 2015-12-05 20:17:42 +08:00
Max Lv d2a1027229 Fix an issue of attach/deattach service when switching NAT mode 2015-12-05 17:32:56 +08:00
Max Lv b0f3d91d5d update shadowsocks-libev 2015-12-03 15:15:29 +08:00
Mygod 873560be7e Remove redundant string resource 2015-12-03 14:12:58 +08:00
Mygod 66f8252326 Remove unused KEY_ACTION 2015-12-03 14:09:58 +08:00
Max Lv 362ec7ebc8 bump version 2015-12-03 11:02:48 +08:00
Max Lv a27336ae67 Simplify the notification title 2015-12-03 09:57:40 +08:00
Max Lv 0ca3d2c8dc fix #463 2015-12-02 12:05:48 +08:00
Max Lv 6725bcc8da Merge pull request #462 from Mygod/master
Refine Tasker features
2015-12-02 07:34:05 +08:00
Mygod e5ce8fd0a6 Refine Tasker features 2015-12-02 00:31:18 +08:00
Max Lv 56a065b182 Merge pull request #461 from Mygod/master
Some fixes
2015-12-01 22:56:45 +08:00
Mygod 41b7da4b63 Fix #460 2015-12-01 22:41:51 +08:00
Mygod b0180fb9a1 Fix notification not updated when service is started and the screen is off 2015-12-01 21:47:35 +08:00
Max Lv ecfbc3e1a7 Merge pull request #459 from CzBiX/tasker
add switch profile feature for Tasker
2015-12-01 18:17:24 +08:00
CzBiX 253cfec33d add switch profile for Tasker 2015-12-01 17:32:49 +08:00
Max Lv a56fb2b522 Merge pull request #457 from Mygod/master
Refine layout
2015-11-30 22:57:44 +08:00
Mygod 52eb2607f9 Fix tasker warning 2015-11-30 21:10:12 +08:00
Mygod 624ccba559 Refine padding 2015-11-30 21:08:17 +08:00
Mygod 7e8a5ac9df Refine AndroidManifest 2015-11-30 21:01:53 +08:00
Mygod e524ab8d2e Refine TaskerActivity title 2015-11-30 20:56:17 +08:00
Mygod c969aed4da Refine layout 2015-11-30 20:54:00 +08:00
Max Lv f891bd1789 Merge pull request #456 from CzBiX/tasker
Automation app integration
2015-11-30 17:08:36 +08:00
CzBiX a6c12dfd5e Remove unused consts in Utils 2015-11-30 16:46:57 +08:00
CzBiX 3d998a326c Automation app integration 2015-11-30 16:23:25 +08:00
Max Lv 1de3a4497c Merge pull request #455 from Mygod/master
Fix recycling in ProfileManagerActivity
2015-11-30 10:03:00 +08:00
Mygod b11280479b Fix recycling in ProfileManagerActivity 2015-11-30 09:45:31 +08:00
Max Lv 2127e17cc7 Merge pull request #454 from Mygod/master
Fix ShadowsocksNotification
2015-11-29 21:41:00 +08:00
Mygod d9179fcc9a Fix ShadowsocksNotification 2015-11-29 18:34:43 +08:00
Mygod b3d188fbae Update libsuperuser 2015-11-29 12:27:09 +08:00
Max Lv c6d2a44c15 Update README.md 2015-11-26 12:02:42 +08:00
Max Lv 2151873264 Merge pull request #449 from Mygod/master
Refine traffic format
2015-11-26 09:58:10 +08:00
Mygod 9b2671d565 Refine traffic format 2015-11-26 09:54:02 +08:00
Mygod f728a67691 Simplify SummaryPreference 2015-11-25 23:35:39 +08:00
Max Lv b269d9a649 Merge pull request #447 from Mygod/master
Show traffic in notification
2015-11-25 09:28:47 +08:00
Mygod 1eda4c47a4 Removed the switch for now 2015-11-24 21:41:09 +08:00
Mygod c1730e4fa6 Add a toggle for showing traffic in notification.
TODO: Disabling it causes weird behavior. :-(
2015-11-24 21:16:08 +08:00
Mygod 9fa57dfe4b Refine service_running string 2015-11-24 19:28:31 +08:00
Mygod 5e0d52324b Add traffic to notification 2015-11-24 19:25:41 +08:00
Mygod 118aa1271c Fix bypass not copied 2015-11-24 18:12:24 +08:00
Mygod 99a6c4ff3e Extract notification logic 2015-11-24 18:11:14 +08:00
Max Lv 19c4910794 ignore all exceptions when upgrading the preference 2015-11-24 14:48:15 +08:00
Max Lv 8b0b39cba7 fix #444 2015-11-24 13:23:16 +08:00
Max Lv be8bc523ad bump version 2015-11-24 10:43:27 +08:00
Max Lv 050007b49b fix an issue when upgrading database 2015-11-24 10:42:57 +08:00
55 changed files with 795 additions and 2865 deletions
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -1 +0,0 @@
sbt.version=0.13.8
+1 -1
View File
@@ -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")
+22 -2
View File
@@ -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) {
}
}
}
+2 -2
View File
@@ -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 \
@@ -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

+22 -36
View File
@@ -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"
+21
View File
@@ -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>
+11 -6
View File
@@ -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>
-3
View File
@@ -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" />
+1 -1
View File
@@ -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>
+13 -6
View File
@@ -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>
+1 -1
View File
@@ -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>
+12 -10
View File
@@ -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))
}
}