diff --git a/app/build.gradle b/app/build.gradle
index 558aeba..a98c5cd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,13 +11,14 @@ buildscript {
}
android {
- compileSdkVersion 19
+ compileSdkVersion 25
buildToolsVersion "23.0.1"
+ useLibrary 'org.apache.http.legacy'
defaultConfig {
applicationId "com.vm.shadowsocks"
- minSdkVersion 14
- targetSdkVersion 19
+ minSdkVersion 15
+ targetSdkVersion 25
versionCode 1
versionName "1.1"
}
@@ -44,6 +45,8 @@ dependencies {
compile 'com.embarkmobile:zxing-android-integration:2.0.0@aar'
compile 'com.google.zxing:core:3.0.1'
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
+ compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
+ compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile('com.googlecode.json-simple:json-simple:1.1.1') {
exclude group: 'junit', module: 'junit'
exclude group: 'org.hamcrest', module: 'hamcrest-core'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0bf10fa..64805b3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,12 @@
+
+
diff --git a/app/src/main/java/com/vm/shadowsocks/core/AppInfo.java b/app/src/main/java/com/vm/shadowsocks/core/AppInfo.java
new file mode 100644
index 0000000..b87268f
--- /dev/null
+++ b/app/src/main/java/com/vm/shadowsocks/core/AppInfo.java
@@ -0,0 +1,36 @@
+package com.vm.shadowsocks.core;
+
+import android.graphics.drawable.Drawable;
+
+public class AppInfo {
+ private Drawable appIcon;
+ private String appLabel;
+ private String pkgName;
+
+ public AppInfo() {
+ }
+
+ public Drawable getAppIcon() {
+ return this.appIcon;
+ }
+
+ public String getAppLabel() {
+ return this.appLabel;
+ }
+
+ public String getPkgName() {
+ return this.pkgName;
+ }
+
+ public void setAppIcon(Drawable var1) {
+ this.appIcon = var1;
+ }
+
+ public void setAppLabel(String var1) {
+ this.appLabel = var1;
+ }
+
+ public void setPkgName(String var1) {
+ this.pkgName = var1;
+ }
+}
diff --git a/app/src/main/java/com/vm/shadowsocks/core/AppProxyManager.java b/app/src/main/java/com/vm/shadowsocks/core/AppProxyManager.java
new file mode 100644
index 0000000..e7ffaee
--- /dev/null
+++ b/app/src/main/java/com/vm/shadowsocks/core/AppProxyManager.java
@@ -0,0 +1,102 @@
+package com.vm.shadowsocks.core;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.content.Context.MODE_PRIVATE;
+
+public class AppProxyManager {
+ public static boolean isLollipopOrAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+
+ public static AppProxyManager Instance;
+ private static final String PROXY_APPS = "PROXY_APPS";
+ private Context mContext;
+
+ public List mlistAppInfo = new ArrayList();
+ public List proxyAppInfo = new ArrayList();
+
+ public AppProxyManager(Context context){
+ this.mContext = context;
+ Instance = this;
+ readProxyAppsList();
+ }
+
+ public void removeProxyApp(String pkg){
+ for (AppInfo app : this.proxyAppInfo) {
+ if (app.getPkgName().equals(pkg)){
+ proxyAppInfo.remove(app);
+ break;
+ }
+ }
+ writeProxyAppsList();
+ }
+
+ public void addProxyApp(String pkg){
+ for (AppInfo app : this.mlistAppInfo) {
+ if (app.getPkgName().equals(pkg)){
+ proxyAppInfo.add(app);
+ break;
+ }
+ }
+ writeProxyAppsList();
+ }
+
+ public boolean isAppProxy(String pkg){
+ for (AppInfo app : this.proxyAppInfo) {
+ if (app.getPkgName().equals(pkg)){
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void readProxyAppsList() {
+ SharedPreferences preferences = mContext.getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE);
+ String tmpString = preferences.getString(PROXY_APPS, "");
+ try {
+ if (proxyAppInfo != null){
+ proxyAppInfo.clear();
+ }
+ if (tmpString.isEmpty()){
+ return;
+ }
+ JSONArray jsonArray = new JSONArray(tmpString);
+ for (int i = 0; i < jsonArray.length() ; i++){
+ JSONObject object = jsonArray.getJSONObject(i);
+ AppInfo appInfo = new AppInfo();
+ appInfo.setAppLabel(object.getString("label"));
+ appInfo.setPkgName(object.getString("pkg"));
+ proxyAppInfo.add(appInfo);
+ }
+ } catch (Exception e){
+ e.printStackTrace();
+ }
+ }
+
+ private void writeProxyAppsList() {
+ SharedPreferences preferences = mContext.getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE);
+ try {
+ JSONArray jsonArray = new JSONArray();
+ for (int i = 0; i < proxyAppInfo.size() ; i++){
+ JSONObject object = new JSONObject();
+ AppInfo appInfo = proxyAppInfo.get(i);
+ object.put("label", appInfo.getAppLabel());
+ object.put("pkg", appInfo.getPkgName());
+ jsonArray.put(object);
+ }
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(PROXY_APPS, jsonArray.toString());
+ editor.apply();
+ } catch (Exception e){
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/vm/shadowsocks/core/LocalVpnService.java b/app/src/main/java/com/vm/shadowsocks/core/LocalVpnService.java
index 3cfa58b..b964edc 100644
--- a/app/src/main/java/com/vm/shadowsocks/core/LocalVpnService.java
+++ b/app/src/main/java/com/vm/shadowsocks/core/LocalVpnService.java
@@ -140,7 +140,7 @@ String getAppInstallID() {
appInstallID = UUID.randomUUID().toString();
Editor editor = preferences.edit();
editor.putString("AppInstallID", appInstallID);
- editor.commit();
+ editor.apply();
}
return appInstallID;
}
@@ -379,12 +379,34 @@ private ParcelFileDescriptor establishVPN() throws Exception {
String value = (String) method.invoke(null, name);
if (value != null && !"".equals(value) && !servers.contains(value)) {
servers.add(value);
- builder.addRoute(value, 32);
+ if (value.replaceAll("\\d", "").length() == 3){//防止IPv6地址导致问题
+ builder.addRoute(value, 32);
+ } else {
+ builder.addRoute(value, 128);
+ }
if (ProxyConfig.IS_DEBUG)
System.out.printf("%s=%s\n", name, value);
}
}
+ if (AppProxyManager.isLollipopOrAbove){
+ if (AppProxyManager.Instance.proxyAppInfo.size() == 0){
+ writeLog("Proxy All Apps");
+ }
+ for (AppInfo app : AppProxyManager.Instance.proxyAppInfo){
+ builder.addAllowedApplication("com.vm.shadowsocks");//需要把自己加入代理,不然会无法进行网络连接
+ try{
+ builder.addAllowedApplication(app.getPkgName());
+ writeLog("Proxy App: " + app.getAppLabel());
+ } catch (Exception e){
+ e.printStackTrace();
+ writeLog("Proxy App Fail: " + app.getAppLabel());
+ }
+ }
+ } else {
+ writeLog("No Pre-App proxy, due to low Android version.");
+ }
+
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
builder.setConfigureIntent(pendingIntent);
diff --git a/app/src/main/java/com/vm/shadowsocks/ui/AppManager.java b/app/src/main/java/com/vm/shadowsocks/ui/AppManager.java
new file mode 100644
index 0000000..d92d14e
--- /dev/null
+++ b/app/src/main/java/com/vm/shadowsocks/ui/AppManager.java
@@ -0,0 +1,201 @@
+package com.vm.shadowsocks.ui;
+
+import android.animation.Animator;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.Switch;
+
+import com.futuremind.recyclerviewfastscroll.FastScroller;
+import com.futuremind.recyclerviewfastscroll.SectionTitleProvider;
+import com.vm.shadowsocks.R;
+import com.vm.shadowsocks.core.AppInfo;
+import com.vm.shadowsocks.core.AppProxyManager;
+
+import java.util.Collections;
+import java.util.List;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.Observer;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+
+/**
+ * Created by so898 on 2017/5/3.
+ */
+
+public class AppManager extends Activity{
+ private View loadingView;
+ private RecyclerView appListView;
+ private FastScroller fastScroller;
+ private AppManagerAdapter adapter;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.setContentView(R.layout.layout_apps);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null)
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ loadingView = findViewById(R.id.loading);
+ appListView = (RecyclerView)findViewById(R.id.list);
+ appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
+ appListView.setItemAnimator(new DefaultItemAnimator());
+ fastScroller = (FastScroller)findViewById(R.id.fastscroller);
+
+ Observable> observable = Observable.create(new ObservableOnSubscribe>() {
+ @Override
+ public void subscribe(ObservableEmitter> appInfo) throws Exception {
+ queryAppInfo();
+ adapter = new AppManagerAdapter();
+ appInfo.onComplete();
+ }
+ }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+ Observer> observer = new Observer>() {
+ @Override
+ public void onSubscribe(Disposable d) {}
+
+ @Override
+ public void onNext(List aLong) {}
+
+ @Override
+ public void onError(Throwable e) {}
+
+ @Override
+ public void onComplete() {
+ appListView.setAdapter(adapter);
+ fastScroller.setRecyclerView(appListView);
+ long shortAnimTime = 1;
+ appListView.setAlpha(0);
+ appListView.setVisibility(View.VISIBLE);
+ appListView.animate().alpha(1).setDuration(shortAnimTime);
+ loadingView.animate().alpha(0).setDuration(shortAnimTime).setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {}
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ loadingView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {}
+ });
+ }
+ };
+ observable.subscribe(observer);
+ }
+
+ public void queryAppInfo() {
+ PackageManager pm = this.getPackageManager(); // 获得PackageManager对象
+ Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+ mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ List resolveInfos = pm.queryIntentActivities(mainIntent, 0);
+ Collections.sort(resolveInfos, new ResolveInfo.DisplayNameComparator(pm));
+ if (AppProxyManager.Instance.mlistAppInfo != null) {
+ AppProxyManager.Instance.mlistAppInfo.clear();
+ for (ResolveInfo reInfo : resolveInfos) {
+ String pkgName = reInfo.activityInfo.packageName; // 获得应用程序的包名
+ String appLabel = (String) reInfo.loadLabel(pm); // 获得应用程序的Label
+ Drawable icon = reInfo.loadIcon(pm); // 获得应用程序图标
+ AppInfo appInfo = new AppInfo();
+ appInfo.setAppLabel(appLabel);
+ appInfo.setPkgName(pkgName);
+ appInfo.setAppIcon(icon);
+ if (!appInfo.getPkgName().equals("com.vm.shadowsocks"))//App本身会强制加入代理列表
+ AppProxyManager.Instance.mlistAppInfo.add(appInfo);
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+}
+
+class AppViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private ImageView icon = (ImageView)itemView.findViewById(R.id.itemicon);
+ private Switch check = (Switch)itemView.findViewById(R.id.itemcheck);
+ private AppInfo item;
+ private Boolean proxied = false;
+
+ AppViewHolder(View itemView) {
+ super(itemView);
+ itemView.setOnClickListener(this);
+ }
+
+ void bind(AppInfo app) {
+ this.item = app;
+ proxied = AppProxyManager.Instance.isAppProxy(app.getPkgName());
+ icon.setImageDrawable(app.getAppIcon());
+ check.setText(app.getAppLabel());
+ check.setChecked(proxied);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (proxied) {
+ AppProxyManager.Instance.removeProxyApp(item.getPkgName());
+ check.setChecked(false);
+ } else {
+ AppProxyManager.Instance.addProxyApp(item.getPkgName());
+ check.setChecked(true);
+ }
+ proxied = !proxied;
+ }
+}
+
+class AppManagerAdapter extends RecyclerView.Adapter implements SectionTitleProvider {
+
+
+ @Override
+ public AppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new AppViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_apps_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(AppViewHolder holder, int position) {
+ AppInfo appInfo = AppProxyManager.Instance.mlistAppInfo.get(position);
+ holder.bind(appInfo);
+ }
+
+ @Override
+ public int getItemCount() {
+ return AppProxyManager.Instance.mlistAppInfo.size();
+ }
+
+ @Override
+ public String getSectionTitle(int position) {
+ AppInfo appInfo = AppProxyManager.Instance.mlistAppInfo.get(position);
+ return appInfo.getAppLabel();
+ }
+}
diff --git a/app/src/main/java/com/vm/shadowsocks/ui/MainActivity.java b/app/src/main/java/com/vm/shadowsocks/ui/MainActivity.java
index cf12e8d..3b85ddd 100644
--- a/app/src/main/java/com/vm/shadowsocks/ui/MainActivity.java
+++ b/app/src/main/java/com/vm/shadowsocks/ui/MainActivity.java
@@ -17,6 +17,7 @@
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
@@ -28,6 +29,8 @@
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.vm.shadowsocks.R;
+import com.vm.shadowsocks.core.AppInfo;
+import com.vm.shadowsocks.core.AppProxyManager;
import com.vm.shadowsocks.core.LocalVpnService;
import com.vm.shadowsocks.core.ProxyConfig;
@@ -49,7 +52,7 @@ public class MainActivity extends Activity implements
private Switch switchProxy;
private TextView textViewLog;
private ScrollView scrollViewLog;
- private TextView textViewProxyUrl;
+ private TextView textViewProxyUrl, textViewProxyApp;
private Calendar mCalendar;
@Override
@@ -60,6 +63,7 @@ protected void onCreate(Bundle savedInstanceState) {
scrollViewLog = (ScrollView) findViewById(R.id.scrollViewLog);
textViewLog = (TextView) findViewById(R.id.textViewLog);
findViewById(R.id.ProxyUrlLayout).setOnClickListener(this);
+ findViewById(R.id.AppSelectLayout).setOnClickListener(this);
textViewProxyUrl = (TextView) findViewById(R.id.textViewProxyUrl);
String ProxyUrl = readProxyUrl();
@@ -74,6 +78,15 @@ protected void onCreate(Bundle savedInstanceState) {
mCalendar = Calendar.getInstance();
LocalVpnService.addOnStatusChangedListener(this);
+
+ //Pre-App Proxy
+ if (AppProxyManager.isLollipopOrAbove){
+ new AppProxyManager(this);
+ textViewProxyApp = (TextView) findViewById(R.id.textViewAppSelectDetail);
+ } else {
+ ((ViewGroup) findViewById(R.id.AppSelectLayout).getParent()).removeView(findViewById(R.id.AppSelectLayout));
+ ((ViewGroup) findViewById(R.id.textViewAppSelectLine).getParent()).removeView(findViewById(R.id.textViewAppSelectLine));
+ }
}
String readProxyUrl() {
@@ -85,7 +98,7 @@ void setProxyUrl(String ProxyUrl) {
SharedPreferences preferences = getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE);
Editor editor = preferences.edit();
editor.putString(CONFIG_URL_KEY, ProxyUrl);
- editor.commit();
+ editor.apply();
}
String getVersionName() {
@@ -129,25 +142,30 @@ public void onClick(View v) {
return;
}
- new AlertDialog.Builder(this)
- .setTitle(R.string.config_url)
- .setItems(new CharSequence[]{
- getString(R.string.config_url_scan),
- getString(R.string.config_url_manual)
- }, new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- switch (i) {
- case 0:
- scanForProxyUrl();
- break;
- case 1:
- showProxyUrlInputDialog();
- break;
+ if (v.getTag().toString().equals("ProxyUrl")){
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.config_url)
+ .setItems(new CharSequence[]{
+ getString(R.string.config_url_scan),
+ getString(R.string.config_url_manual)
+ }, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ switch (i) {
+ case 0:
+ scanForProxyUrl();
+ break;
+ case 1:
+ showProxyUrlInputDialog();
+ break;
+ }
}
- }
- })
- .show();
+ })
+ .show();
+ } else if (v.getTag().toString().equals("AppSelect")){
+ System.out.println("abc");
+ startActivity(new Intent(this, AppManager.class));
+ }
}
private void scanForProxyUrl() {
@@ -352,6 +370,20 @@ public void onClick(DialogInterface dialog, int which) {
}
}
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (AppProxyManager.isLollipopOrAbove) {
+ if (AppProxyManager.Instance.proxyAppInfo.size() != 0) {
+ String tmpString = "";
+ for (AppInfo app : AppProxyManager.Instance.proxyAppInfo) {
+ tmpString += app.getAppLabel() + ", ";
+ }
+ textViewProxyApp.setText(tmpString);
+ }
+ }
+ }
+
@Override
protected void onDestroy() {
LocalVpnService.removeOnStatusChangedListener(this);
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index e0a17e5..8614ba8 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -8,6 +8,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
+ android:tag="ProxyUrl"
android:padding="13dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_apps_item.xml b/app/src/main/res/layout/layout_apps_item.xml
new file mode 100644
index 0000000..408e329
--- /dev/null
+++ b/app/src/main/res/layout/layout_apps_item.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-v11/styles.xml b/app/src/main/res/values-v11/styles.xml
index 3c02242..972ddeb 100644
--- a/app/src/main/res/values-v11/styles.xml
+++ b/app/src/main/res/values-v11/styles.xml
@@ -8,4 +8,10 @@
+
+
diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml
index 3e61a52..bb46641 100644
--- a/app/src/main/res/values-v14/styles.xml
+++ b/app/src/main/res/values-v14/styles.xml
@@ -9,4 +9,10 @@
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 108e7ec..1626884 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -10,6 +10,9 @@
扫描二维码
手动输入
扫描配置地址的二维码
+ 选择使用代理的软件
+ 未选择
+ 使用代理的软件
确定
取消
更多…
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8f510f7..1396c8c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -10,6 +10,9 @@
Scan an QR code
Type manually
Scan the QR code of your config url
+ Select App
+ No
+ Per-App Proxy
OK
Cancel
More…
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7c28a60..c4880b0 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -17,4 +17,10 @@
+
+