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 @@ + +