Commit 4ddebb7c authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1027 from shadowsocks/drawer

New UI with drawer (#762)
parents 5596b8ac 8cb0d0d7
...@@ -22,6 +22,7 @@ proguardOptions ++= ...@@ -22,6 +22,7 @@ proguardOptions ++=
"-keep class com.github.shadowsocks.System { *; }" :: "-keep class com.github.shadowsocks.System { *; }" ::
"-dontwarn com.google.android.gms.internal.**" :: "-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" :: "-dontwarn com.j256.ormlite.**" ::
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" :: "-dontwarn org.xbill.**" ::
Nil Nil
...@@ -31,17 +32,22 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW") ...@@ -31,17 +32,22 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
resolvers += Resolver.jcenterRepo resolvers += Resolver.jcenterRepo
libraryDependencies ++= libraryDependencies ++=
"com.android.support" % "cardview-v7" % "25.1.0" ::
"com.android.support" % "design" % "25.1.0" :: "com.android.support" % "design" % "25.1.0" ::
"com.android.support" % "gridlayout-v7" % "25.1.0" :: "com.android.support" % "gridlayout-v7" % "25.1.0" ::
"com.android.support" % "preference-v14" % "25.1.0" :: "com.android.support" % "preference-v14" % "25.1.0" ::
"com.evernote" % "android-job" % "1.1.3" :: "com.evernote" % "android-job" % "1.1.3" ::
"com.github.clans" % "fab" % "1.6.4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.github.kevinsawicki" % "http-request" % "6.0" ::
"com.google.android.gms" % "play-services-ads" % "10.0.1" :: "com.google.android.gms" % "play-services-ads" % "10.0.1" ::
"com.google.android.gms" % "play-services-analytics" % "10.0.1" :: "com.google.android.gms" % "play-services-analytics" % "10.0.1" ::
"com.google.android.gms" % "play-services-gcm" % "10.0.1" :: "com.google.android.gms" % "play-services-gcm" % "10.0.1" ::
"com.j256.ormlite" % "ormlite-android" % "5.0" :: "com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.1.5" ::
"com.mikepenz" % "iconics-core" % "2.8.2" ::
"com.mikepenz" % "materialdrawer" % "5.8.1" ::
"com.mikepenz" % "materialize" % "1.0.0" ::
"com.squareup.okhttp3" % "okhttp" % "3.5.0" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.7" :: "dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/> android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<activity <activity
android:name=".Shadowsocks" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
...@@ -49,28 +49,6 @@ ...@@ -49,28 +49,6 @@
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ShadowsocksRunnerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask">
</activity>
<activity
android:name=".ProfileManagerActivity"
android:label="@string/profiles"
android:parentActivityName=".Shadowsocks"
android:excludeFromRecents="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.github.shadowsocks.ProfileManagerActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
...@@ -82,18 +60,33 @@ ...@@ -82,18 +60,33 @@
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" /> <data android:scheme="ss" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity> </activity>
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="@string/profile_config"
android:launchMode="singleTask"/>
<activity
android:name=".ShadowsocksRunnerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
<activity <activity
android:name=".ScannerActivity" android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code" android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".ProfileManagerActivity" android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/> android:excludeFromRecents="true"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
android:parentActivityName=".Shadowsocks" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
...@@ -104,18 +97,6 @@ ...@@ -104,18 +97,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ShadowsocksQuickSwitchActivity"
android:label="@string/quick_switch"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:taskAffinity=""
android:theme="@style/Theme.Material.Dialog">
<intent-filter>
<action android:name="com.github.shadowsocks.QUICK_SWITCH" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity <activity
android:name=".QuickToggleShortcut" android:name=".QuickToggleShortcut"
android:label="@string/quick_toggle" android:label="@string/quick_toggle"
......
...@@ -7,6 +7,8 @@ interface IShadowsocksService { ...@@ -7,6 +7,8 @@ interface IShadowsocksService {
String getProfileName(); String getProfileName();
oneway void registerCallback(IShadowsocksServiceCallback cb); oneway void registerCallback(IShadowsocksServiceCallback cb);
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb); oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in int profileId); oneway void use(in int profileId);
......
package com.github.shadowsocks;
import android.app.Activity;
/**
* This helper class is used to solve Java-Scala interop problem. Use onAttach(Context) and remove this class
* when minSdkVersion >= 23.
*
* @author Mygod
*/
public class DialogFragment extends android.app.DialogFragment {
protected void superOnAttach(Activity activity) {
super.onAttach(activity);
}
}
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
<corners android:radius="4dp"/>
</shape>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_green_700" />
<corners android:radius="4dp"/>
</shape>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
<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="#FF000000"
android:pathData="M10.08,10.86c0.05,-0.33 0.16,-0.62 0.3,-0.87s0.34,-0.46 0.59,-0.62c0.24,-0.15 0.54,-0.22 0.91,-0.23 0.23,0.01 0.44,0.05 0.63,0.13 0.2,0.09 0.38,0.21 0.52,0.36s0.25,0.33 0.34,0.53 0.13,0.42 0.14,0.64h1.79c-0.02,-0.47 -0.11,-0.9 -0.28,-1.29s-0.4,-0.73 -0.7,-1.01 -0.66,-0.5 -1.08,-0.66 -0.88,-0.23 -1.39,-0.23c-0.65,0 -1.22,0.11 -1.7,0.34s-0.88,0.53 -1.2,0.92 -0.56,0.84 -0.71,1.36S8,11.29 8,11.87v0.27c0,0.58 0.08,1.12 0.23,1.64s0.39,0.97 0.71,1.35 0.72,0.69 1.2,0.91 1.05,0.34 1.7,0.34c0.47,0 0.91,-0.08 1.32,-0.23s0.77,-0.36 1.08,-0.63 0.56,-0.58 0.74,-0.94 0.29,-0.74 0.3,-1.15h-1.79c-0.01,0.21 -0.06,0.4 -0.15,0.58s-0.21,0.33 -0.36,0.46 -0.32,0.23 -0.52,0.3c-0.19,0.07 -0.39,0.09 -0.6,0.1 -0.36,-0.01 -0.66,-0.08 -0.89,-0.23 -0.25,-0.16 -0.45,-0.37 -0.59,-0.62s-0.25,-0.55 -0.3,-0.88 -0.08,-0.67 -0.08,-1v-0.27c0,-0.35 0.03,-0.68 0.08,-1.01zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector> </vector>
<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="#FF000000"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector> </vector>
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z"/> android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector> </vector>
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FF000000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/> android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector> </vector>
<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="M7,10l5,5 5,-5z"/>
</vector>
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector> </vector>
<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="#FF000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
<?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"/>
<WebView android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?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:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/background_header"/>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="shadowsocks"
android:gravity="center"
android:textSize="32sp"
android:textColor="@android:color/white"/>
</RelativeLayout>
This diff is collapsed.
<?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" />
<fragment android:id="@+id/content"
class="com.github.shadowsocks.ProfileConfigFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="match_parent"
android:layout_height="match_parent"> android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" /> <include layout="@layout/toolbar_light_dark"/>
<android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="0dp" android:layout_weight="1"> android:id="@+id/profilesList"
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="0dp"
android:layout_height="match_parent"/> android:layout_weight="1"
<com.github.clans.fab.FloatingActionMenu android:paddingBottom="32dp"
android:id="@+id/menu" android:clipChildren="false"
android:layout_width="match_parent" android:clipToPadding="false"/>
android:layout_height="match_parent"
android:layout_gravity="bottom|end"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
app:menu_colorNormal="@color/material_primary_500"
app:menu_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:layout_behavior="com.github.shadowsocks.widget.FloatingActionMenuBehavior">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_manual_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_manual_settings"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_qrcode_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_scan_qr_code"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_nfc_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="NFC"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_import_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/action_import"
/>
</com.github.clans.fab.FloatingActionMenu>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/master/core/res/res/layout/select_dialog_singlechoice_material.xml --> <!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:id="@+id/container" <com.github.shadowsocks.widget.BoundedCardView android:layout_width="match_parent"
android:background="?android:attr/selectableItemBackground" android:layout_height="wrap_content"
android:paddingStart="20dp" android:background="@drawable/background_selectable"
android:paddingEnd="?attr/dialogPreferredPadding" android:layout_marginTop="8dp"
android:focusable="true" android:layout_marginStart="8dp"
android:nextFocusRight="@+id/share"> android:layout_marginEnd="8dp"
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_gravity="center_horizontal"
android:id="@android:id/text1" app:bounded_width="@dimen/profile_item_max_width"
android:layout_width="0dp" android:id="@+id/indicator">
android:layout_height="wrap_content" <LinearLayout android:layout_width="match_parent"
android:layout_gravity="center_vertical" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall" android:padding="8dp"
android:textAppearance="?android:attr/textAppearanceMedium" android:focusable="true"
android:textColor="?android:attr/textColorAlertDialogListItem" android:nextFocusRight="@+id/edit"
android:gravity="center_vertical" android:id="@+id/content"
android:drawableStart="?android:attr/listChoiceIndicatorSingle" android:background="?android:attr/selectableItemBackground">
android:drawablePadding="20dp" <LinearLayout android:layout_width="match_parent"
android:maxLines="4" android:layout_height="wrap_content"
android:scrollHorizontally="false" /> android:layout_marginStart="8dp"
<ImageView android:id="@+id/share" android:id="@+id/container">
android:layout_width="wrap_content" <TextView android:id="@+id/title"
android:layout_height="wrap_content" android:layout_width="0dp"
android:padding="5dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
app:srcCompat="@drawable/ic_social_share" android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:textAppearance="?android:attr/textAppearanceMedium"
android:contentDescription="@string/share" android:textColor="?android:attr/textColorPrimary"
android:focusable="true" android:gravity="center_vertical"
android:nextFocusLeft="@+id/container"/> android:maxLines="2"
</LinearLayout> android:ellipsize="end"/>
<android.support.v7.widget.AppCompatImageView android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share"
android:focusable="true"
android:nextFocusLeft="@+id/container"/>
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit"
android:focusable="true"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:id="@+id/details">
<TextView android:id="@+id/address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="top"
android:layout_weight="1"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
<TextView android:id="@+id/traffic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="top"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</LinearLayout>
</com.github.shadowsocks.widget.BoundedCardView>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/profilesList"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_delete"
android:title="@string/delete"
android:icon="@drawable/ic_action_delete"
android:alphabeticShortcut="d"
android:numericShortcut="2"
app:showAsAction="always"/>
<item android:id="@+id/action_apply"
android:title="@string/apply"
android:icon="@drawable/ic_action_done"
android:alphabeticShortcut="a"
android:numericShortcut="3"
app:showAsAction="always"/>
</menu>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_profile"
android:icon="@drawable/ic_action_note_add"
android:alphabeticShortcut="a"
android:numericShortcut="0"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_scan_qr_code"
android:title="@string/add_profile_methods_scan_qr_code"
android:alphabeticShortcut="s"
android:numericShortcut="1"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="2"/>
<item android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="3"/>
</menu>
</item>
<item android:id="@+id/action_export" <item android:id="@+id/action_export"
android:title="@string/action_export" android:title="@string/action_export"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_qr_code_nfc"
android:title="@string/share_qr_nfc"
android:alphabeticShortcut="q"
android:numericShortcut="1"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:alphabeticShortcut="e"
android:numericShortcut="2"/>
</menu>
...@@ -38,7 +38,6 @@ ...@@ -38,7 +38,6 @@
<string name="nat_no_root">NATモードはROOT権限が必要です</string> <string name="nat_no_root">NATモードはROOT権限が必要です</string>
<string name="switch_to_vpn">VPNモードに切り替える</string> <string name="switch_to_vpn">VPNモードに切り替える</string>
<string name="stop">中止</string> <string name="stop">中止</string>
<string name="quick_switch">切り替え</string>
<string name="stopping">停止中……</string> <string name="stopping">停止中……</string>
<string name="vpn_error">バックグランドサービスの起動に失敗しました:%s</string> <string name="vpn_error">バックグランドサービスの起動に失敗しました:%s</string>
<string name="reboot_required">VPNサービスの起動に失敗しました、デバイスの再起動を試みて下さい</string> <string name="reboot_required">VPNサービスの起動に失敗しました、デバイスの再起動を試みて下さい</string>
...@@ -52,7 +51,6 @@ ...@@ -52,7 +51,6 @@
<string name="settings">オプション設定</string> <string name="settings">オプション設定</string>
<string name="remove_profile">このプロファイルを削除「%s」?</string> <string name="remove_profile">このプロファイルを削除「%s」?</string>
<string name="recovery">リセット</string> <string name="recovery">リセット</string>
<string name="recovery_summary">すべてのバックグラウンドサービスをリセット</string>
<string name="about">本アプリについて</string> <string name="about">本アプリについて</string>
<string name="about_title">シャドーソックス(shadowsocks)%s</string> <string name="about_title">シャドーソックス(shadowsocks)%s</string>
<string name="flush_dnscache">DNS キャッシュをリフレッシュ</string> <string name="flush_dnscache">DNS キャッシュをリフレッシュ</string>
...@@ -60,16 +58,11 @@ ...@@ -60,16 +58,11 @@
<string name="flush_dnscache_no_root">ROOT権限がない場合はDNSキャッシュをリフレッシュできません</string> <string name="flush_dnscache_no_root">ROOT権限がない場合はDNSキャッシュをリフレッシュできません</string>
<string name="flushing">リフレッシュ中……</string> <string name="flushing">リフレッシュ中……</string>
<string name="share">共有</string> <string name="share">共有</string>
<string name="share_message">QRコードを読み取る或いはAndroid Beam (NFC) を使用</string>
<string name="share_message_without_nfc">QRコードを読み取る</string>
<string name="share_message_nfc_disabled">QRコードを読み取る\nAndroid Beam が無効になっています</string>
<string name="turn_on_nfc">NFC 及び Android Beam を有効にする</string>
<string name="add_profile">プロファイルを追加</string> <string name="add_profile">プロファイルを追加</string>
<string name="nat">NAT モード (デバックのみ)</string> <string name="nat">NAT モード (デバックのみ)</string>
<string name="nat_summary">VPNモードからNATモードに切り替えるにはROOT権限が必要です</string> <string name="nat_summary">VPNモードからNATモードに切り替えるにはROOT権限が必要です</string>
<string name="remote_dns">リモートDNS</string> <string name="remote_dns">リモートDNS</string>
<string name="remote_dns_summary">リモート解析に使用するDNS(デフォルトネームサーバー 8.8.8.8)</string>
<string name="udp_dns">UDP転送</string> <string name="udp_dns">UDP転送</string>
<string name="udp_dns_summary">UDPプロトコルでリモードサーバーにパケットを転送</string> <string name="udp_dns_summary">UDPプロトコルでリモードサーバーにパケットを転送</string>
...@@ -80,7 +73,6 @@ ...@@ -80,7 +73,6 @@
<string name="ipv6">IPv6プロキシ</string> <string name="ipv6">IPv6プロキシ</string>
<string name="ipv6_summary">リモートサーバーにIPv6パケットを転送</string> <string name="ipv6_summary">リモートサーバーにIPv6パケットを転送</string>
<string name="stat">ネットワークトラフィック</string>
<string name="stat_summary">送信済み: \t%3$s\t↑\t%1$s/s\n受信済み: \t%4$s\t↓\t%2$s/s</string> <string name="stat_summary">送信済み: \t%3$s\t↑\t%1$s/s\n受信済み: \t%4$s\t↓\t%2$s/s</string>
<string name="connection_test_pending">接続状況確認</string> <string name="connection_test_pending">接続状況確認</string>
...@@ -91,16 +83,10 @@ ...@@ -91,16 +83,10 @@
<string name="connection_test_error_status_code">ステータスコード無効(#%d)</string> <string name="connection_test_error_status_code">ステータスコード無効(#%d)</string>
<!-- profile --> <!-- profile -->
<string name="profile_manager_dialog">プロファイル</string> <string name="copy_url">URL をコピー</string>
<string name="profile_manager_dialog_content">&#8226; タッチしてプロファイルを選択;\n&#8226; 左或いは右にスワイプして削除;\n&#8226;
長押し後にドラッグで順序を変更</string>
<string name="gotcha">承知</string>
<string name="copy_url">URLをコピー</string>
<string name="add_profile_dialog">シャドーソックスにプロファイルを追加しますか</string> <string name="add_profile_dialog">シャドーソックスにプロファイルを追加しますか</string>
<string name="add_profile_methods_scan_qr_code">QRコードを読み取る</string> <string name="add_profile_methods_scan_qr_code">QRコードを読み取る</string>
<string name="add_profile_methods_manual_settings">手動でセットアップ</string> <string name="add_profile_methods_manual_settings">手動でセットアップ</string>
<string name="add_profile_nfc_hint_title">Android Beam (NFC) でプロファイルを追加</string>
<string name="add_profile_nfc_hint">2台のデバイス画面を立ち上げ、もう一方のデバイスでプロファイルの共有アイコンをタッチし、2台とも背中合わせにして下さい\nNFC またはNFCラベルを直接スキャン</string>
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string> <string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
<string-array name="route_entry"> <string-array name="route_entry">
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
<string name="profile_summary">Переключить на другой профиль или добавить новые</string> <string name="profile_summary">Переключить на другой профиль или добавить новые</string>
<string name="nat">Режим NAT (не рекомендуется)</string> <string name="nat">Режим NAT (не рекомендуется)</string>
<string name="nat_summary">Использовать режим NAT вместо VPN. Требует ROOT прав.</string> <string name="nat_summary">Использовать режим NAT вместо VPN. Требует ROOT прав.</string>
<string name="stat">Сетевая Активность</string>
<string name="stat_summary">Отправлено: \t\t\t\t\t%3$s\t↑\t%1$s/s\nПолучено: \t%4$s\t↓\t%2$s/s</string> <string name="stat_summary">Отправлено: \t\t\t\t\t%3$s\t↑\t%1$s/s\nПолучено: \t%4$s\t↓\t%2$s/s</string>
<string name="stat_profiles">\n%1$s↑\t%2$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> <string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
...@@ -74,7 +73,6 @@ ...@@ -74,7 +73,6 @@
<string name="stopping">Останавливается…</string> <string name="stopping">Останавливается…</string>
<string name="vpn_error">%s</string> <string name="vpn_error">%s</string>
<string name="reboot_required">Сбой запуска VPN сервиса. Требуется перезагрузка Вашего устройства.</string> <string name="reboot_required">Сбой запуска VPN сервиса. Требуется перезагрузка Вашего устройства.</string>
<string name="quick_switch">Переключить</string>
<!-- alert category --> <!-- alert category -->
<string name="close">Закрыть</string> <string name="close">Закрыть</string>
...@@ -89,14 +87,9 @@ ...@@ -89,14 +87,9 @@
<string name="profiles">Профили</string> <string name="profiles">Профили</string>
<string name="settings">Настройки</string> <string name="settings">Настройки</string>
<string name="recovery">Сброс</string> <string name="recovery">Сброс</string>
<string name="recovery_summary">Сброс всех фоновых задач</string>
<string name="about">О приложении</string> <string name="about">О приложении</string>
<string name="about_title">Shadowsocks %s</string> <string name="about_title">Shadowsocks %s</string>
<string name="share">Поделиться</string> <string name="share">Поделиться</string>
<string name="share_message">Сканировать QR код или использовать Android Beam (NFC)</string>
<string name="share_message_without_nfc">Сканировать QR код</string>
<string name="share_message_nfc_disabled">Сканировать QR код\nAndroid Beam отключен</string>
<string name="turn_on_nfc">Включить NFC &amp; Android Beam</string>
<string name="flush_dnscache">Очистить кэш DNS</string> <string name="flush_dnscache">Очистить кэш DNS</string>
<string name="flush_dnscache_summary">Работает только в режиме NAT. Требует наличия ROOT прав.</string> <string name="flush_dnscache_summary">Работает только в режиме NAT. Требует наличия ROOT прав.</string>
<string name="flush_dnscache_no_root">Очистка кэша DNS не работает без налиичя ROOT прав</string> <string name="flush_dnscache_no_root">Очистка кэша DNS не работает без налиичя ROOT прав</string>
...@@ -112,15 +105,9 @@ ...@@ -112,15 +105,9 @@
<string name="action_import_err">Сбой импорта.</string> <string name="action_import_err">Сбой импорта.</string>
<!-- profile --> <!-- profile -->
<string name="profile_manager_dialog">Profiles</string>
<string name="profile_manager_dialog_content">&#8226; Нажмите для выбора профиля;\n&#8226; Сдвиньте влево/вправо или нажмите влево
чтобы удалить;\n&#8226; Подержите чтобы изменить порядок профилей.</string>
<string name="gotcha">Gotcha!</string>
<string name="add_profile_dialog">Добавить этот Профиль Shadowsocks?</string> <string name="add_profile_dialog">Добавить этот Профиль Shadowsocks?</string>
<string name="add_profile_methods_scan_qr_code">Сканировать QR код</string> <string name="add_profile_methods_scan_qr_code">Сканировать QR код</string>
<string name="add_profile_methods_manual_settings">Настройка вручную</string> <string name="add_profile_methods_manual_settings">Настройка вручную</string>
<string name="add_profile_nfc_hint_title">Использовать Android Beam (NFC) для добавления профиля</string>
<string name="add_profile_nfc_hint">Оставьте экран включённым на обоих устройствах, на втором устойстве нажмите кнопку профиля "поделиться", поднисите устройства друг к другу (тыльными сторонами).\nТак же Вы можете сканировать nfc метку.</string>
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string> <string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
<string name="copy_url">Копировать URL</string> <string name="copy_url">Копировать URL</string>
<plurals name="removed"> <plurals name="removed">
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="fab_margin_bottom">-28dp</dimen> <dimen name="fab_margin_top">-28dp</dimen>
</resources> </resources>
...@@ -38,11 +38,13 @@ ...@@ -38,11 +38,13 @@
<string name="nat_no_root">NAT 模式需要 ROOT 权限</string> <string name="nat_no_root">NAT 模式需要 ROOT 权限</string>
<string name="switch_to_vpn">切换到 VPN 模式</string> <string name="switch_to_vpn">切换到 VPN 模式</string>
<string name="stop">停止</string> <string name="stop">停止</string>
<string name="quick_switch">切换</string>
<string name="stopping">正在关闭……</string> <string name="stopping">正在关闭……</string>
<string name="vpn_error">后台服务启动失败:%s</string> <string name="vpn_error">后台服务启动失败:%s</string>
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string> <string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
<string name="yes"></string>
<string name="no"></string>
<string name="close">关闭</string> <string name="close">关闭</string>
<string name="profile_empty">请选择配置文件</string>
<string name="proxy_empty">代理服务器地址及密码不能为空</string> <string name="proxy_empty">代理服务器地址及密码不能为空</string>
<string name="connect">连接</string> <string name="connect">连接</string>
<string name="initializing">正在初始化……</string> <string name="initializing">正在初始化……</string>
...@@ -52,24 +54,19 @@ ...@@ -52,24 +54,19 @@
<string name="settings">设置选项</string> <string name="settings">设置选项</string>
<string name="remove_profile">删除此配置文件「%s」?</string> <string name="remove_profile">删除此配置文件「%s」?</string>
<string name="recovery">重置</string> <string name="recovery">重置</string>
<string name="recovery_summary">重置所有后台服务</string>
<string name="about">关于</string> <string name="about">关于</string>
<string name="about_title">影梭(shadowsocks)%s</string> <string name="about_title">影梭(shadowsocks)%s</string>
<string name="flush_dnscache">刷新 DNS 缓存</string> <string name="flush_dnscache">刷新 DNS 缓存</string>
<string name="flush_dnscache_summary">只需在 NAT 模式下使用,需要 ROOT 权限</string> <string name="flush_dnscache_summary">只需在 NAT 模式下使用,需要 ROOT 权限</string>
<string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string> <string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string>
<string name="flushing">刷新中……</string> <string name="flushing">刷新中……</string>
<string name="edit">编辑</string>
<string name="share">分享</string> <string name="share">分享</string>
<string name="share_message">扫描二维码或使用 Android Beam (NFC)</string>
<string name="share_message_without_nfc">扫描二维码</string>
<string name="share_message_nfc_disabled">扫描二维码\nAndroid Beam 被禁用</string>
<string name="turn_on_nfc">打开 NFC &amp; Android Beam</string>
<string name="add_profile">添加配置文件</string> <string name="add_profile">添加配置文件</string>
<string name="nat">NAT 模式 (仅限调试)</string> <string name="nat">NAT 模式 (仅限调试)</string>
<string name="nat_summary">从 VPN 模式切换为 NAT 模式,需要 ROOT 权限</string> <string name="nat_summary">从 VPN 模式切换为 NAT 模式,需要 ROOT 权限</string>
<string name="remote_dns">远程 DNS</string> <string name="remote_dns">远程 DNS</string>
<string name="remote_dns_summary">用于远程域名解析的 DNS 服务器 (默认为 8.8.8.8)</string>
<string name="udp_dns">UDP 转发</string> <string name="udp_dns">UDP 转发</string>
<string name="udp_dns_summary">由远程服务器转发 UDP 协议的数据包</string> <string name="udp_dns_summary">由远程服务器转发 UDP 协议的数据包</string>
...@@ -80,7 +77,6 @@ ...@@ -80,7 +77,6 @@
<string name="ipv6">IPv6 路由</string> <string name="ipv6">IPv6 路由</string>
<string name="ipv6_summary">向远程服务器转发 IPv6 流量</string> <string name="ipv6_summary">向远程服务器转发 IPv6 流量</string>
<string name="stat">网络流量</string>
<string name="stat_summary">发送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string> <string name="stat_summary">发送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string>
<string name="connection_test_pending">检查网络连接</string> <string name="connection_test_pending">检查网络连接</string>
...@@ -91,16 +87,16 @@ ...@@ -91,16 +87,16 @@
<string name="connection_test_error_status_code">状态码无效(#%d)</string> <string name="connection_test_error_status_code">状态码无效(#%d)</string>
<!-- profile --> <!-- profile -->
<string name="profile_manager_dialog">配置文件</string> <string name="profile_config">配置文件设置</string>
<string name="profile_manager_dialog_content">&#8226; 点击选择配置文件;\n&#8226; 向左/右滑动或按左删除;\n&#8226; <string name="unsaved_changes_prompt">保存修改吗?</string>
长按拖动改变顺序。</string> <string name="apply">应用</string>
<string name="gotcha">好的</string> <string name="delete">删除</string>
<string name="delete_confirm_prompt">你是否确定要删除这个配置文件?</string>
<string name="share_qr_nfc">二维码 / NFC</string>
<string name="copy_url">复制 URL</string> <string name="copy_url">复制 URL</string>
<string name="add_profile_dialog">为影梭添加此配置文件?</string> <string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string> <string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string> <string name="add_profile_methods_manual_settings">手动设置</string>
<string name="add_profile_nfc_hint_title">使用 Android Beam (NFC) 来添加配置</string>
<string name="add_profile_nfc_hint">保持两台设备的屏幕亮起,在另外一台设备上,点击配置文件的分享按钮,将它们靠在一起(背靠背)。\n你也可以扫描 NFC 标签。</string>
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string> <string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
<string-array name="route_entry"> <string-array name="route_entry">
...@@ -134,6 +130,10 @@ ...@@ -134,6 +130,10 @@
<!-- status --> <!-- status -->
<string name="sent">发送:</string> <string name="sent">发送:</string>
<string name="received">接收:</string> <string name="received">接收:</string>
<string name="connecting">连接中…</string>
<string name="vpn_connected">已连接, 轻触以检查连接状态</string>
<string name="nat_connected">已连接</string>
<string name="not_connected">未连接</string>
<!-- kcp --> <!-- kcp -->
<string name="kcp_summary">启用 KCP 协议</string> <string name="kcp_summary">启用 KCP 协议</string>
......
...@@ -39,11 +39,13 @@ ...@@ -39,11 +39,13 @@
<string name="nat_no_root">NAT 模式需要 ROOT 權限</string> <string name="nat_no_root">NAT 模式需要 ROOT 權限</string>
<string name="switch_to_vpn">切換至 VPN 模式</string> <string name="switch_to_vpn">切換至 VPN 模式</string>
<string name="stop">停止</string> <string name="stop">停止</string>
<string name="quick_switch">切換</string>
<string name="stopping">關閉中……</string> <string name="stopping">關閉中……</string>
<string name="vpn_error">%s</string> <string name="vpn_error">%s</string>
<string name="reboot_required">VPN 服務啟動失敗。您或許需要重新啟動您的裝置。</string> <string name="reboot_required">VPN 服務啟動失敗。您或許需要重新啟動您的裝置。</string>
<string name="yes"></string>
<string name="no"></string>
<string name="close">關閉</string> <string name="close">關閉</string>
<string name="profile_empty">請選擇設定檔</string>
<string name="proxy_empty">Proxy 或密碼不可以空白</string> <string name="proxy_empty">Proxy 或密碼不可以空白</string>
<string name="connect">連線</string> <string name="connect">連線</string>
<string name="initializing">起始中……</string> <string name="initializing">起始中……</string>
...@@ -53,24 +55,19 @@ ...@@ -53,24 +55,19 @@
<string name="settings">設定</string> <string name="settings">設定</string>
<string name="remove_profile">移除此設定檔 "%s"?</string> <string name="remove_profile">移除此設定檔 "%s"?</string>
<string name="recovery">重設</string> <string name="recovery">重設</string>
<string name="recovery_summary">重設一切背景服務</string>
<string name="about">關於</string> <string name="about">關於</string>
<string name="about_title">Shadowsocks %s</string> <string name="about_title">Shadowsocks %s</string>
<string name="flush_dnscache">清除 DNS 快取</string> <string name="flush_dnscache">清除 DNS 快取</string>
<string name="flush_dnscache_summary">僅在 NAT 模式可用,需要 ROOT 權限</string> <string name="flush_dnscache_summary">僅在 NAT 模式可用,需要 ROOT 權限</string>
<string name="flush_dnscache_no_root">在無 ROOT 權限時清除 DNS 快取無法進行</string> <string name="flush_dnscache_no_root">在無 ROOT 權限時清除 DNS 快取無法進行</string>
<string name="flushing">清除中……</string> <string name="flushing">清除中……</string>
<string name="edit">編輯</string>
<string name="share">分享</string> <string name="share">分享</string>
<string name="share_message">掃描 QR 碼或者使用 Android Beam (NFC)</string>
<string name="share_message_without_nfc">掃描 QR 碼</string>
<string name="share_message_nfc_disabled">掃描 QR 碼\nAndroid Beam 被停用</string>
<string name="turn_on_nfc">啟用 NFC &amp; Android Beam</string>
<string name="add_profile">新增設定檔</string> <string name="add_profile">新增設定檔</string>
<string name="nat">NAT 模式 (已過時)</string> <string name="nat">NAT 模式 (已過時)</string>
<string name="nat_summary">使用 NAT 模式代替 VPN 模式,需要 ROOT 權限</string> <string name="nat_summary">使用 NAT 模式代替 VPN 模式,需要 ROOT 權限</string>
<string name="remote_dns">遠程 DNS</string> <string name="remote_dns">遠程 DNS</string>
<string name="remote_dns_summary">用於遠程域名解析的 DNS 伺服器 (默認為 8.8.8.8) </string>
<string name="udp_dns">UDP 轉送</string> <string name="udp_dns">UDP 轉送</string>
<string name="udp_dns_summary">向遠端轉送 UDP 封包</string> <string name="udp_dns_summary">向遠端轉送 UDP 封包</string>
...@@ -81,7 +78,6 @@ ...@@ -81,7 +78,6 @@
<string name="ipv6">IPv6 路由</string> <string name="ipv6">IPv6 路由</string>
<string name="ipv6_summary">向遠端重新導向 IPv6 流量</string> <string name="ipv6_summary">向遠端重新導向 IPv6 流量</string>
<string name="stat">網路流量</string>
<string name="stat_summary">傳送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string> <string name="stat_summary">傳送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string>
<string name="connection_test_pending">檢查連線能力</string> <string name="connection_test_pending">檢查連線能力</string>
...@@ -92,17 +88,16 @@ ...@@ -92,17 +88,16 @@
<string name="connection_test_error_status_code">錯誤碼: #%d</string> <string name="connection_test_error_status_code">錯誤碼: #%d</string>
<!-- profile --> <!-- profile -->
<string name="profile_manager_dialog">設定檔</string> <string name="profile_config">設定檔配置</string>
<string name="profile_manager_dialog_content">&#8226; 輕觸選擇一個設定檔;\n&#8226; 向左/右撥動或按左移除;\n&#8226; <string name="unsaved_changes_prompt">要保存更改嗎?</string>
按住後上下撥動設定檔重新排序。</string> <string name="apply">應用</string>
<string name="gotcha">我懂了!</string> <string name="delete">刪除</string>
<string name="delete_confirm_prompt">您確定要移除這個設定檔嗎?</string>
<string name="share_qr_nfc">QR 碼 / NFC</string>
<string name="copy_url">複製 URL</string> <string name="copy_url">複製 URL</string>
<string name="add_profile_dialog">為 Shadowsocks 新增此設定檔?</string> <string name="add_profile_dialog">為 Shadowsocks 新增此設定檔?</string>
<string name="add_profile_methods_scan_qr_code">掃描 QR 碼</string> <string name="add_profile_methods_scan_qr_code">掃描 QR 碼</string>
<string name="add_profile_methods_manual_settings">手動設定</string> <string name="add_profile_methods_manual_settings">手動設定</string>
<string name="add_profile_nfc_hint_title">使用 Android Beam (NFC) 來新增設定檔</string>
<string name="add_profile_nfc_hint">
保持兩台設備的屏幕亮起,在另外一台設備上,點擊設定檔的分享按鈕,將它們靠在一起(背靠背)。\n你也可以掃描 NFC 標籤。</string>
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string> <string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
<string-array name="route_entry"> <string-array name="route_entry">
...@@ -136,6 +131,10 @@ ...@@ -136,6 +131,10 @@
<!-- status --> <!-- status -->
<string name="sent">傳送:</string> <string name="sent">傳送:</string>
<string name="received">接收:</string> <string name="received">接收:</string>
<string name="connecting">連線中…</string>
<string name="vpn_connected">已連線, 輕觸以檢查連線能力</string>
<string name="nat_connected">已連線</string>
<string name="not_connected">未連線</string>
<!-- kcp --> <!-- kcp -->
<string name="kcp_summary">啟用 KCP 協定</string> <string name="kcp_summary">啟用 KCP 協定</string>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="BoundedView">
<attr name="bounded_width" format="dimension" />
<attr name="bounded_height" format="dimension" />
</declare-styleable>
<declare-styleable name="NumberPickerPreference"> <declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" /> <attr name="max" format="integer" />
<attr name="min" format="integer" /> <attr name="min" format="integer" />
......
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
<resources> <resources>
<color name="material_green_700">#388E3C</color> <color name="material_green_700">#388E3C</color>
<color name="material_green_a700">#00C853</color>
<color name="material_blue_grey_300">#90A4AE</color> <color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color> <color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</color> <color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_300">@color/material_blue_grey_300</color> <color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color> <color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_700">@color/material_blue_grey_700</color> <color name="material_primary_700">@color/material_blue_grey_700</color>
<color name="material_accent_200">@color/material_blue_grey_500</color> <color name="material_accent_200">@color/material_green_a700</color>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="profile_item_max_width">480dp</dimen>
<!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 --> <!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 -->
<dimen name="fab_margin_bottom">-44dp</dimen> <dimen name="fab_margin_top">-44dp</dimen>
</resources> </resources>
...@@ -11,10 +11,8 @@ ...@@ -11,10 +11,8 @@
<string name="nat">NAT mode (deprecated)</string> <string name="nat">NAT mode (deprecated)</string>
<string name="nat_summary">Use NAT mode instead of VPN mode. Requires ROOT permission.</string> <string name="nat_summary">Use NAT mode instead of VPN mode. Requires ROOT permission.</string>
<string name="remote_dns">Remote DNS</string> <string name="remote_dns">Remote DNS</string>
<string name="remote_dns_summary">DNS server for remote hostname resolution. (Default 8.8.8.8)</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_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="stat_profiles">%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string> <string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
<plurals name="bytes" translatable="false"> <plurals name="bytes" translatable="false">
<item quantity="one">Byte</item> <item quantity="one">Byte</item>
...@@ -82,10 +80,13 @@ ...@@ -82,10 +80,13 @@
<string name="stopping">Shutting down…</string> <string name="stopping">Shutting down…</string>
<string name="vpn_error">%s</string> <string name="vpn_error">%s</string>
<string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string> <string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string>
<string name="quick_switch">Switch</string> <string name="profile_invalid_input">No valid profile data found.</string>
<!-- alert category --> <!-- alert category -->
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string> <string name="proxy_empty">Proxy/Password should not be empty</string>
<string name="connect">Connect</string> <string name="connect">Connect</string>
<string name="initializing">Initializing…</string> <string name="initializing">Initializing…</string>
...@@ -97,14 +98,10 @@ ...@@ -97,14 +98,10 @@
<string name="profiles">Profiles</string> <string name="profiles">Profiles</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="recovery">Reset</string> <string name="recovery">Reset</string>
<string name="recovery_summary">Reset all the background services</string>
<string name="about">About</string> <string name="about">About</string>
<string name="about_title">Shadowsocks %s</string> <string name="about_title">Shadowsocks %s</string>
<string name="edit">Edit</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="share_message">Scan QR code or use Android Beam (NFC)</string>
<string name="share_message_without_nfc">Scan QR code</string>
<string name="share_message_nfc_disabled">Scan QR code\nAndroid Beam is disabled</string>
<string name="turn_on_nfc">Turn on NFC &amp; Android Beam</string>
<string name="flush_dnscache">Flush DNS cache</string> <string name="flush_dnscache">Flush DNS cache</string>
<string name="flush_dnscache_summary">Only useful in NAT mode. Requires ROOT permission.</string> <string name="flush_dnscache_summary">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="flush_dnscache_no_root">Flush DNS cache cannot proceed without ROOT permission</string>
...@@ -119,15 +116,15 @@ ...@@ -119,15 +116,15 @@
<string name="action_import_err">Failed to import.</string> <string name="action_import_err">Failed to import.</string>
<!-- profile --> <!-- profile -->
<string name="profile_manager_dialog">Profiles</string> <string name="profile_config">Profile config</string>
<string name="profile_manager_dialog_content">&#8226; Tap to select a profile;\n&#8226; Swipe left/right or press left <string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
to remove;\n&#8226; Long press to reorder profiles.</string> <string name="apply">Apply</string>
<string name="gotcha">Gotcha!</string> <string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code/NFC</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string> <string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string> <string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string> <string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="add_profile_nfc_hint_title">Use Android Beam (NFC) to add profile</string>
<string name="add_profile_nfc_hint">Keep both devices screen on, on the other device, click profile\'s share button, bring them together (back to back).\nYou also can scan a nfc tag.</string>
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string> <string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
<string name="copy_url">Copy URL</string> <string name="copy_url">Copy URL</string>
<plurals name="removed"> <plurals name="removed">
...@@ -146,6 +143,10 @@ ...@@ -146,6 +143,10 @@
<!-- status --> <!-- status -->
<string name="sent">Sent:</string> <string name="sent">Sent:</string>
<string name="received">Received:</string> <string name="received">Received:</string>
<string name="connecting">Connecting…</string>
<string name="vpn_connected">Connected, tap to check connection</string>
<string name="nat_connected">Connected</string>
<string name="not_connected">Not connected</string>
<!-- kcp --> <!-- kcp -->
<string name="kcp" translatable="false">KCP</string> <string name="kcp" translatable="false">KCP</string>
......
...@@ -6,14 +6,16 @@ ...@@ -6,14 +6,16 @@
<item name="android:textSize">24sp</item> <item name="android:textSize">24sp</item>
</style> </style>
<style name="Theme.MaterialBase" parent="Theme.AppCompat.Light.NoActionBar"> <style name="Theme.MaterialBase" parent="MaterialDrawerTheme.Light.DarkToolbar">
<!--suppress NewApi --> <!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item> <item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="colorAccent">@color/material_primary_500</item> <item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_primary_500</item> <item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item> <item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item> <item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
<item name="material_drawer_selected_text">@color/material_accent_200</item>
</style> </style>
<style name="Theme.Material.Dialog" parent="Theme.AppCompat.Light.Dialog"> <style name="Theme.Material.Dialog" parent="Theme.AppCompat.Light.Dialog">
......
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
<SwitchPreference android:key="isNAT"
android:title="@string/nat"
android:summary="@string/nat_summary"/>
</PreferenceScreen>
...@@ -7,44 +7,37 @@ ...@@ -7,44 +7,37 @@
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="profileName" android:key="profileName"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/profile_name"/> android:title="@string/profile_name"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="proxy" android:key="proxy"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/proxy"/> android:title="@string/proxy"/>
<be.mygod.preference.NumberPickerPreference <be.mygod.preference.NumberPickerPreference
app:min="1" app:min="1"
app:max="65535" app:max="65535"
android:key="remotePortNum" android:key="remotePortNum"
android:persistent="false"
android:summary="%d" android:summary="%d"
android:title="@string/remote_port"/> android:title="@string/remote_port"/>
<be.mygod.preference.NumberPickerPreference <be.mygod.preference.NumberPickerPreference
app:min="1025" app:min="1025"
app:max="65535" app:max="65535"
android:key="localPortNum" android:key="localPortNum"
android:persistent="false"
android:summary="%d" android:summary="%d"
android:title="@string/port"/> android:title="@string/port"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:inputType="textPassword" android:inputType="textPassword"
android:key="sitekey" android:key="sitekey"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/sitekey"/> android:title="@string/sitekey"/>
<DropDownPreference <DropDownPreference
android:key="encMethod" android:key="encMethod"
android:persistent="false"
android:entries="@array/enc_method_entry" android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value" android:entryValues="@array/enc_method_value"
android:summary="%s" android:summary="%s"
android:title="@string/enc_method"/> android:title="@string/enc_method"/>
<SwitchPreference <SwitchPreference
android:key="isAuth" android:key="isAuth"
android:persistent="false"
android:summary="@string/onetime_auth_summary" android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/> android:title="@string/onetime_auth"/>
...@@ -55,7 +48,6 @@ ...@@ -55,7 +48,6 @@
<DropDownPreference <DropDownPreference
android:key="route" android:key="route"
android:persistent="false"
android:entries="@array/route_entry" android:entries="@array/route_entry"
android:entryValues="@array/route_value" android:entryValues="@array/route_value"
android:summary="%s" android:summary="%s"
...@@ -63,20 +55,17 @@ ...@@ -63,20 +55,17 @@
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="remoteDns" android:key="remoteDns"
android:title="@string/remote_dns" android:title="@string/remote_dns"
android:summary="@string/remote_dns_summary"/> android:summary="%s"/>
<SwitchPreference <SwitchPreference
android:key="isIpv6" android:key="isIpv6"
android:persistent="false"
android:summary="@string/ipv6_summary" android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/> android:title="@string/ipv6"/>
<SwitchPreference <SwitchPreference
android:key="isProxyApps" android:key="isProxyApps"
android:persistent="false"
android:summary="@string/proxied_apps_summary" android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/> android:title="@string/proxied_apps"/>
<SwitchPreference <SwitchPreference
android:key="isUdpDns" android:key="isUdpDns"
android:persistent="false"
android:summary="@string/udp_dns_summary" android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/> android:title="@string/udp_dns"/>
...@@ -87,7 +76,6 @@ ...@@ -87,7 +76,6 @@
<SwitchPreference <SwitchPreference
android:key="kcp" android:key="kcp"
android:persistent="false"
android:summary="@string/kcp_summary" android:summary="@string/kcp_summary"
android:title="@string/kcp"/> android:title="@string/kcp"/>
...@@ -95,41 +83,14 @@ ...@@ -95,41 +83,14 @@
app:min="1" app:min="1"
app:max="65535" app:max="65535"
android:key="kcpPort" android:key="kcpPort"
android:persistent="false"
android:summary="@string/kcp_port_summary" android:summary="@string/kcp_port_summary"
android:title="@string/kcp_port"/> android:title="@string/kcp_port"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="kcpcli" android:key="kcpcli"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/kcpcli"/> android:title="@string/kcpcli"/>
</be.mygod.preference.PreferenceCategory> </be.mygod.preference.PreferenceCategory>
<be.mygod.preference.PreferenceCategory android:title="@string/misc_cat">
<SwitchPreference
android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
<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="about" android:title="@string/about"/>
</be.mygod.preference.PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>
...@@ -8,14 +8,6 @@ ...@@ -8,14 +8,6 @@
android:targetPackage="com.github.shadowsocks" android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.QuickToggleShortcut" /> android:targetClass="com.github.shadowsocks.QuickToggleShortcut" />
</shortcut> </shortcut>
<shortcut android:shortcutId="switch"
android:icon="@drawable/ic_qu_shadowsocks_launcher"
android:shortcutShortLabel="@string/quick_switch"
android:shortcutLongLabel="@string/quick_switch">
<intent android:action="android.intent.action.MAIN"
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.ShadowsocksQuickSwitchActivity" />
</shortcut>
<shortcut android:shortcutId="scan" <shortcut android:shortcutId="scan"
android:icon="@drawable/ic_qu_camera_launcher" android:icon="@drawable/ic_qu_camera_launcher"
android:shortcutShortLabel="@string/add_profile_methods_scan_qr_code" android:shortcutShortLabel="@string/add_profile_methods_scan_qr_code"
......
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.util.Locale
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.webkit.{WebView, WebViewClient}
class AboutFragment extends ToolbarFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_about, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
val web = view.findViewById(R.id.web_view).asInstanceOf[WebView]
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))) catch {
case _: android.content.ActivityNotFoundException => // Ignore
}
true
}
})
}
}
...@@ -98,10 +98,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -98,10 +98,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName) proxiedApps.add(item.packageName)
check.setChecked(true) check.setChecked(true)
} }
if (!appsLoading.get) { if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
profile.individual = proxiedApps.mkString("\n")
app.profileManager.updateProfile(profile)
}
} }
} }
...@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private var loadingView: View = _ private var loadingView: View = _
private val appsLoading = new AtomicBoolean private val appsLoading = new AtomicBoolean
private var handler: Handler = _ private var handler: Handler = _
private val profile = app.currentProfile.orNull
private def initProxiedApps(str: String = profile.individual) = proxiedApps = str.split('\n').to[mutable.HashSet] private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) =
proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() { override def onDestroy() {
instance = null instance = null
...@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all => case R.id.action_apply_all =>
app.profileManager.getAllProfiles match { app.profileManager.getAllProfiles match {
case Some(profiles) => case Some(profiles) =>
val proxiedAppString = profile.individual val proxiedAppString = app.settings.getString(Key.individual, null)
profiles.foreach(p => { profiles.foreach(p => {
p.individual = proxiedAppString p.individual = proxiedAppString
app.profileManager.updateProfile(p) app.profileManager.updateProfile(p)
...@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
} }
return true return true
case R.id.action_export => case R.id.action_export =>
val bypass = profile.bypass val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = profile.individual val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString) val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
...@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "") val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean) bypassSwitch.setChecked(enabled.toBoolean)
profile.individual = apps app.editor.putString(Key.individual, apps).apply()
app.profileManager.updateProfile(profile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE) appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE) loadingView.setVisibility(View.VISIBLE)
...@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
protected override def onCreate(savedInstanceState: Bundle) { protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (profile == null) finish()
handler = new Handler() handler = new Handler()
this.setContentView(R.layout.layout_apps) this.setContentView(R.layout.layout_apps)
...@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu) toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (!profile.proxyApps) { if (!app.settings.getBoolean(Key.proxyApps, false)) app.editor.putBoolean(Key.proxyApps, true).apply()
profile.proxyApps = true
app.profileManager.updateProfile(profile)
}
findViewById(R.id.onSwitch).asInstanceOf[Switch] findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => { .setOnCheckedChangeListener((_, checked) => {
profile.proxyApps = checked app.editor.putBoolean(Key.proxyApps, checked).apply()
app.profileManager.updateProfile(profile)
finish() finish()
}) })
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch] bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(profile.bypass) bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) => { bypassSwitch.setOnCheckedChangeListener((_, checked) => app.editor.putBoolean(Key.bypass, checked).apply())
profile.bypass = checked
app.profileManager.updateProfile(profile)
})
initProxiedApps() initProxiedApps()
loadingView = findViewById(R.id.loading) loadingView = findViewById(R.id.loading)
......
...@@ -21,20 +21,26 @@ ...@@ -21,20 +21,26 @@
package com.github.shadowsocks package com.github.shadowsocks
import java.io.IOException import java.io.IOException
import java.net.InetAddress
import java.util
import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask} import java.util.{Timer, TimerTask}
import android.app.Service import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.{Handler, RemoteCallbackList} import android.os.{Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.github.kevinsawicki.http.HttpRequest
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback} import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import okhttp3.{Dns, FormBody, OkHttpClient, Request}
import scala.collection.mutable
import scala.util.Random
trait BaseService extends Service { trait BaseService extends Service {
...@@ -49,7 +55,7 @@ trait BaseService extends Service { ...@@ -49,7 +55,7 @@ trait BaseService extends Service {
var trafficMonitorThread: TrafficMonitorThread = _ var trafficMonitorThread: TrafficMonitorThread = _
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback] final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
var callbacksCount: Int = _ private final val bandwidthListeners = new mutable.HashSet[IBinder]() // the binder is the real identifier
lazy val handler = new Handler(getMainLooper) lazy val handler = new Handler(getMainLooper)
lazy val restartHanlder = new Handler(getMainLooper) lazy val restartHanlder = new Handler(getMainLooper)
lazy val protectPath: String = getApplicationInfo.dataDir + "/protect_path" lazy val protectPath: String = getApplicationInfo.dataDir + "/protect_path"
...@@ -78,38 +84,36 @@ trait BaseService extends Service { ...@@ -78,38 +84,36 @@ trait BaseService extends Service {
state state
} }
override def getProfileName: String = if (profile == null) null else profile.name override def getProfileName: String = if (profile == null) null else profile.getName
override def unregisterCallback(cb: IShadowsocksServiceCallback) { override def registerCallback(cb: IShadowsocksServiceCallback): Unit = callbacks.register(cb)
if (cb != null && callbacks.unregister(cb)) {
callbacksCount -= 1
if (callbacksCount == 0 && timer != null) {
timer.cancel()
timer = null
}
}
}
override def registerCallback(cb: IShadowsocksServiceCallback) { override def startListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
if (cb != null && callbacks.register(cb)) { if (bandwidthListeners.add(cb.asBinder)){
callbacksCount += 1 if (timer == null) {
if (callbacksCount != 0 && timer == null) {
val task = new TimerTask {
def run() {
if (TrafficMonitor.updateRate()) updateTrafficRate()
}
}
timer = new Timer(true) timer = new Timer(true)
timer.schedule(task, 1000, 1000) timer.schedule(new TimerTask {
def run(): Unit = if (state == State.CONNECTED && TrafficMonitor.updateRate()) updateTrafficRate()
}, 1000, 1000)
} }
TrafficMonitor.updateRate() TrafficMonitor.updateRate()
cb.trafficUpdated(TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal) cb.trafficUpdated(TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
} }
override def stopListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
if (bandwidthListeners.remove(cb.asBinder) && bandwidthListeners.isEmpty) {
timer.cancel()
timer = null
}
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb)
} }
override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else { override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else {
val profile = app.profileManager.getProfile(profileId).orNull val profile = app.profileManager.getProfile(profileId).orNull
if (profile == null) stopRunner(stopService = true) else state match { if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match {
case State.STOPPED => if (checkProfile(profile)) startRunner(profile) case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) { case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
stopRunner(stopService = false) stopRunner(stopService = false)
...@@ -132,13 +136,28 @@ trait BaseService extends Service { ...@@ -132,13 +136,28 @@ trait BaseService extends Service {
val container = holder.getContainer val container = holder.getContainer
val url = container.getString("proxy_url") val url = container.getString("proxy_url")
val sig = Utils.getSignature(this) val sig = Utils.getSignature(this)
val list = HttpRequest
.post(url) val client = new OkHttpClient.Builder()
.connectTimeout(2000) .dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
.readTimeout(2000) case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
.send("sig="+sig) case _ => Dns.SYSTEM.lookup(hostname)
.body })
val proxies = util.Random.shuffle(list.split('|').toSeq) .connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val requestBody = new FormBody.Builder()
.add("sig", sig)
.build()
val request = new Request.Builder()
.url(url)
.post(requestBody)
.build()
val resposne = client.newCall(request).execute()
val list = resposne.body.string
val proxies = Random.shuffle(list.split('|').toSeq)
val proxy = proxies.head.split(':') val proxy = proxies.head.split(':')
profile.host = proxy(0).trim profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt profile.remotePort = proxy(1).trim.toInt
...@@ -238,7 +257,7 @@ trait BaseService extends Service { ...@@ -238,7 +257,7 @@ trait BaseService extends Service {
def updateTrafficRate() { def updateTrafficRate() {
handler.post(() => { handler.post(() => {
if (callbacksCount > 0) { if (bandwidthListeners.nonEmpty) {
val txRate = TrafficMonitor.txRate val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal val txTotal = TrafficMonitor.txTotal
...@@ -246,7 +265,8 @@ trait BaseService extends Service { ...@@ -246,7 +265,8 @@ trait BaseService extends Service {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i <- 0 until n) { for (i <- 0 until n) {
try { try {
callbacks.getBroadcastItem(i).trafficUpdated(txRate, rxRate, txTotal, rxTotal) val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder)) item.trafficUpdated(txRate, rxRate, txTotal, rxTotal)
} catch { } catch {
case _: Exception => // Ignore case _: Exception => // Ignore
} }
...@@ -259,7 +279,7 @@ trait BaseService extends Service { ...@@ -259,7 +279,7 @@ trait BaseService extends Service {
override def onCreate() { override def onCreate() {
super.onCreate() super.onCreate()
app.refreshContainerHolder app.refreshContainerHolder()
app.updateAssets() app.updateAssets()
} }
...@@ -268,8 +288,8 @@ trait BaseService extends Service { ...@@ -268,8 +288,8 @@ trait BaseService extends Service {
protected def changeState(s: Int, msg: String = null) { protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper) val handler = new Handler(getMainLooper)
handler.post(() => if (state != s || msg != null) { if (state != s || msg != null) {
if (callbacksCount > 0) { if (callbacks.getRegisteredCallbackCount > 0) handler.post(() => {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i <- 0 until n) { for (i <- 0 until n) {
try { try {
...@@ -279,9 +299,9 @@ trait BaseService extends Service { ...@@ -279,9 +299,9 @@ trait BaseService extends Service {
} }
} }
callbacks.finishBroadcast() callbacks.finishBroadcast()
} })
state = s state = s
}) }
} }
def getBlackList: String = { def getBlackList: String = {
......
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
class GlobalConfigFragment extends PreferenceFragment {
override def onCreatePreferences(bundle: Bundle, key: String) {
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(getActivity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
tfo.setChecked(TcpFastOpen.sendEnabled)
tfo.setOnPreferenceChangeListener((_, v) => {
val value = v.asInstanceOf[Boolean]
val result = TcpFastOpen.enabled(value)
if (result != null && result != "Success.")
Snackbar.make(getActivity.findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
value == TcpFastOpen.sendEnabled
})
if (!TcpFastOpen.supported) {
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.app.Fragment
class GlobalSettingsFragment extends ToolbarFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_global_settings, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.settings)
val fm = getChildFragmentManager
fm.beginTransaction().replace(R.id.content, new GlobalConfigFragment()).commit()
fm.executePendingTransactions()
}
override def onDetach() {
super.onDetach()
try {
val childFragmentManager = classOf[Fragment].getDeclaredField("mChildFragmentManager")
childFragmentManager.setAccessible(true)
childFragmentManager.set(this, null)
} catch {
case _: Exception => // ignore
}
}
}
...@@ -20,77 +20,33 @@ ...@@ -20,77 +20,33 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.content.pm.ShortcutManager import android.app.Activity
import android.content.res.Resources import android.content.DialogInterface
import android.os.{Build, Bundle} import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AlertDialog
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar} import android.support.v7.widget.Toolbar
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.CheckedTextView
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.Utils
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
/** class ProfileConfigActivity extends Activity {
* Created by Lucas on 3/10/16. private lazy val child = getFragmentManager.findFragmentById(R.id.content).asInstanceOf[ProfileConfigFragment]
*/
class ShadowsocksQuickSwitchActivity extends AppCompatActivity {
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
{
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
}
private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
itemView.setOnClickListener(this)
def bind(item: Profile) {
this.item = item
text.setText(item.name)
text.setChecked(item.id == app.profileId)
}
def onClick(v: View) {
app.switchProfile(item.id)
Utils.startSsService(ShadowsocksQuickSwitchActivity.this)
finish()
}
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
val profiles: List[Profile] = app.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
def getItemCount: Int = profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int): Unit = i match {
case _ => vh.bind(profiles(i))
}
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
def onCreateViewHolder(vg: ViewGroup, i: Int) = new ProfileViewHolder(LayoutInflater.from(vg.getContext)
.inflate(Resources.getSystem.getIdentifier(name, "layout", "android"), vg, false))
}
private val profilesAdapter = new ProfilesAdapter
override def onCreate(savedInstanceState: Bundle) { override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.layout_quick_switch) setContentView(R.layout.layout_profile_config)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.quick_switch) toolbar.setTitle(R.string.profile_config)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView] toolbar.setNavigationOnClickListener(_ => onBackPressed())
val lm = new LinearLayoutManager(this) toolbar.inflateMenu(R.menu.profile_config_menu)
profilesList.setLayoutManager(lm) toolbar.setOnMenuItemClickListener(child)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
if (app.profileId >= 0) lm.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
}.getOrElse(0))
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("switch")
} }
override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
.setNeutralButton(android.R.string.cancel, null)
.create()
.show() else super.onBackPressed()
} }
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.{DialogInterface, Intent, SharedPreferences}
import android.os.{Build, Bundle, UserManager}
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.Preference
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.view.MenuItem
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
import com.github.shadowsocks.utils.{Action, Key, Utils}
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
with OnSharedPreferenceChangeListener {
private var profile: Profile = _
private var isProxyApps: SwitchPreference = _
override def onCreatePreferences(bundle: Bundle, key: String) {
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) =>
profile = p
profile.serialize(app.editor).apply()
case None => getActivity.finish()
}
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && getActivity.getSystemService(classOf[UserManager]).isDemoUser) {
findPreference(Key.host).setSummary("shadowsocks.example.org")
findPreference(Key.remotePort).setSummary("1337")
findPreference(Key.password).setSummary("\u2022" * 32)
}
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setEnabled(Utils.isLollipopOrAbove || app.isNatEnabled)
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(getActivity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
app.settings.registerOnSharedPreferenceChangeListener(this)
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroy()
}
override def onResume() {
super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = preference.getKey match {
case Key.kcpcli => displayPreferenceDialog(Key.kcpcli, new KcpCliPreferenceDialogFragment())
case _ => super.onDisplayPreferenceDialog(preference)
}
override def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_delete =>
new AlertDialog.Builder(getActivity)
.setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, ((_, _) => {
app.profileManager.delProfile(profile.id)
getActivity.finish()
}): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, null)
.create()
.show()
true
case R.id.action_apply =>
saveAndExit()
true
case _ => false
}
def saveAndExit() {
profile.deserialize(app.settings)
app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.refreshId(profile.id)
getActivity.finish()
}
}
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.nio.charset.Charset
import android.app.Activity
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter}
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ImageView, LinearLayout}
import com.github.shadowsocks.utils.Utils
import net.glxn.qrgen.android.QRCode
final class QRCodeDialog(url: String) extends DialogFragment {
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
private var adapter: NfcAdapter = _
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = {
val image = new ImageView(getActivity)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(getActivity, 250), Utils.dpToPx(getActivity, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
image
}
override def onAttach(activity: Activity) {
superOnAttach(activity)
adapter = NfcAdapter.getDefaultAdapter(getActivity)
if (adapter != null) adapter.setNdefPushMessage(new NdefMessage(Array(
new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem))), activity)
}
override def onDetach() {
if (adapter != null) {
adapter.setNdefPushMessage(null, getActivity)
adapter = null
}
super.onDetach()
}
}
...@@ -58,20 +58,13 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand ...@@ -58,20 +58,13 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand
} }
} }
def navigateUp() {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
}
override def onCreate(state: Bundle) { override def onCreate(state: Bundle) {
super.onCreate(state) super.onCreate(state)
setContentView(R.layout.layout_scanner) setContentView(R.layout.layout_scanner)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getTitle) toolbar.setTitle(getTitle)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material) toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => navigateUp()) toolbar.setNavigationOnClickListener(_ => finish())
scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView] scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan") if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
} }
...@@ -98,7 +91,9 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand ...@@ -98,7 +91,9 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand
val uri = rawResult.getText val uri = rawResult.getText
if (!TextUtils.isEmpty(uri)) if (!TextUtils.isEmpty(uri))
Parser.findAll(uri).foreach(app.profileManager.createProfile) Parser.findAll(uri).foreach(app.profileManager.createProfile)
navigateUp() val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
} }
} }
...@@ -30,12 +30,18 @@ import com.github.shadowsocks.ShadowsocksApplication.app ...@@ -30,12 +30,18 @@ import com.github.shadowsocks.ShadowsocksApplication.app
* @author Mygod * @author Mygod
*/ */
trait ServiceBoundContext extends Context with IBinder.DeathRecipient { trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
class ShadowsocksServiceConnection extends ServiceConnection { private class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) { override def onServiceConnected(name: ComponentName, service: IBinder) {
binder = service binder = service
service.linkToDeath(ServiceBoundContext.this, 0) service.linkToDeath(ServiceBoundContext.this, 0)
bgService = IShadowsocksService.Stub.asInterface(service) bgService = IShadowsocksService.Stub.asInterface(service)
registerCallback() if (callback != null && !callbackRegistered) try {
bgService.registerCallback(callback)
callbackRegistered = true
if (listeningForBandwidth) bgService.startListeningForBandwidth(callback)
} catch {
case _: RemoteException => // Nothing
}
ServiceBoundContext.this.onServiceConnected() ServiceBoundContext.this.onServiceConnected()
} }
override def onServiceDisconnected(name: ComponentName) { override def onServiceDisconnected(name: ComponentName) {
...@@ -46,33 +52,37 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -46,33 +52,37 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
} }
} }
protected def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try { protected def setListeningForBandwidth(value: Boolean) {
bgService.registerCallback(callback) if (listeningForBandwidth != value && bgService != null && callback != null)
callbackRegistered = true if (value) bgService.startListeningForBandwidth(callback) else bgService.stopListeningForBandwidth(callback)
} catch { listeningForBandwidth = value
case _: RemoteException => // Nothing
} }
protected def unregisterCallback() { private def unregisterCallback() {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch { if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case _: RemoteException => case _: RemoteException =>
} }
listeningForBandwidth = false
callbackRegistered = false callbackRegistered = false
} }
def onServiceConnected(): Unit = () protected def onServiceConnected(): Unit = ()
def onServiceDisconnected(): Unit = () /**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
protected def onServiceDisconnected(): Unit = ()
override def binderDied(): Unit = () override def binderDied(): Unit = ()
private var callback: IShadowsocksServiceCallback.Stub = _ private var callback: IShadowsocksServiceCallback.Stub = _
private var connection: ShadowsocksServiceConnection = _ private var connection: ShadowsocksServiceConnection = _
private var callbackRegistered: Boolean = _ private var callbackRegistered: Boolean = _
private var listeningForBandwidth: Boolean = _
// Variables // Variables
var binder: IBinder = _ private var binder: IBinder = _
var bgService: IShadowsocksService = _ var bgService: IShadowsocksService = _
def attachService(callback: IShadowsocksServiceCallback.Stub = null) { protected def attachService(callback: IShadowsocksServiceCallback.Stub = null) {
this.callback = callback this.callback = callback
if (bgService == null) { if (bgService == null) {
val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService] val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
...@@ -85,8 +95,9 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -85,8 +95,9 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
} }
} }
def detachService() { protected def detachService() {
unregisterCallback() unregisterCallback()
onServiceDisconnected()
callback = null callback = null
if (connection != null) { if (connection != null) {
try unbindService(connection) catch { try unbindService(connection) catch {
......
...@@ -83,13 +83,14 @@ class ShadowsocksApplication extends Application { ...@@ -83,13 +83,14 @@ class ShadowsocksApplication extends Application {
.setFatal(false) .setFatal(false)
.build()) .build())
def profileId: Int = settings.getInt(Key.id, -1) def profileId: Int = settings.getInt(Key.id, 0)
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply() def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
def currentProfile: Option[Profile] = profileManager.getProfile(profileId) def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
def switchProfile(id: Int): Profile = { def switchProfile(id: Int): Profile = {
profileId(id) val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
profileManager.getProfile(id) getOrElse profileManager.createProfile() profileId(result.id)
result
} }
private def checkChineseLocale(locale: Locale): Locale = if (locale.getLanguage == "zh") locale.getCountry match { private def checkChineseLocale(locale: Locale): Locale = if (locale.getLanguage == "zh") locale.getCountry match {
......
...@@ -315,7 +315,7 @@ class ShadowsocksNatService extends BaseService { ...@@ -315,7 +315,7 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route) AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED) changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name, true) notification = new ShadowsocksNotification(this, profile.getName, true)
} }
override def stopRunner(stopService: Boolean, msg: String = null) { override def stopRunner(stopService: Boolean, msg: String = null) {
......
...@@ -24,13 +24,12 @@ import java.util.Locale ...@@ -24,13 +24,12 @@ import java.util.Locale
import android.app.{KeyguardManager, NotificationManager, PendingIntent} import android.app.{KeyguardManager, NotificationManager, PendingIntent}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.PowerManager import android.os.{Build, PowerManager}
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{TrafficMonitor, Action, State, Utils} import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
import com.github.shadowsocks.ShadowsocksApplication.app
/** /**
* @author Mygod * @author Mygod
...@@ -57,17 +56,11 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str ...@@ -57,17 +56,11 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setColor(ContextCompat.getColor(service, R.color.material_primary_500)) .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success)) .setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName) .setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks]) .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity])
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_stat_shadowsocks) .setSmallIcon(R.drawable.ic_stat_shadowsocks)
builder.addAction(R.drawable.ic_navigation_close, if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0)) service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
app.profileManager.getAllProfiles match {
case Some(profiles) => if (profiles.length > 1)
builder.addAction(R.drawable.ic_action_settings, service.getString(R.string.quick_switch),
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
case _ =>
}
private lazy val style = new BigTextStyle(builder) private lazy val style = new BigTextStyle(builder)
private var isVisible = true private var isVisible = true
update(if (service.getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager].isScreenOn) update(if (service.getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager].isScreenOn)
...@@ -87,6 +80,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str ...@@ -87,6 +80,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
case Intent.ACTION_SCREEN_ON => case Intent.ACTION_SCREEN_ON =>
setVisible(visible && Utils.isLollipopOrAbove && !keyGuard.inKeyguardRestrictedInputMode, forceShow) setVisible(visible && Utils.isLollipopOrAbove && !keyGuard.inKeyguardRestrictedInputMode, forceShow)
service.binder.registerCallback(callback) service.binder.registerCallback(callback)
service.binder.startListeningForBandwidth(callback)
callbackRegistered = true callbackRegistered = true
case Intent.ACTION_USER_PRESENT => setVisible(visible = true, forceShow = forceShow) case Intent.ACTION_USER_PRESENT => setVisible(visible = true, forceShow = forceShow)
} }
......
...@@ -148,7 +148,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -148,7 +148,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Route.ALL) if (profile.route != Route.ALL)
AclSyncJob.schedule(profile.route) AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.name) notification = new ShadowsocksNotification(this, profile.getName)
} }
/** Called when the activity is first created. */ /** Called when the activity is first created. */
...@@ -318,7 +318,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -318,7 +318,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val builder = new Builder() val builder = new Builder()
builder builder
.setSession(profile.name) .setSession(profile.getName)
.setMtu(VPN_MTU) .setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24) .addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
......
...@@ -52,7 +52,7 @@ class TaskerActivity extends AppCompatActivity { ...@@ -52,7 +52,7 @@ class TaskerActivity extends AppCompatActivity {
} }
def bind(item: Profile) { def bind(item: Profile) {
this.item = item this.item = item
text.setText(item.name) text.setText(item.getName)
text.setChecked(taskerOption.profileId == item.id) text.setChecked(taskerOption.profileId == item.id)
} }
......
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Fragment
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.view.View
class ToolbarFragment extends Fragment {
var toolbar: Toolbar = _
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
val activity = getActivity.asInstanceOf[MainActivity]
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
def onTrafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
}
...@@ -23,8 +23,10 @@ package com.github.shadowsocks.database ...@@ -23,8 +23,10 @@ package com.github.shadowsocks.database
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
import android.content.SharedPreferences
import android.os.Binder import android.os.Binder
import android.util.Base64 import android.util.Base64
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField} import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile { class Profile {
...@@ -32,10 +34,10 @@ class Profile { ...@@ -32,10 +34,10 @@ class Profile {
var id: Int = 0 var id: Int = 0
@DatabaseField @DatabaseField
var name: String = "Untitled" var name: String = ""
@DatabaseField @DatabaseField
var host: String = "" var host: String = "198.199.101.152"
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
@DatabaseField @DatabaseField
...@@ -45,7 +47,7 @@ class Profile { ...@@ -45,7 +47,7 @@ class Profile {
var remotePort: Int = 8388 var remotePort: Int = 8388
@DatabaseField @DatabaseField
var password: String = "" var password: String = "u1rRWTssNv0p"
@DatabaseField @DatabaseField
var method: String = "aes-256-cfb" var method: String = "aes-256-cfb"
...@@ -95,9 +97,53 @@ class Profile { ...@@ -95,9 +97,53 @@ class Profile {
@DatabaseField @DatabaseField
var kcpcli: String = "--crypt none --mode normal --mtu 1200 --nocomp --dscp 46 --parityshard 0" var kcpcli: String = "--crypt none --mode normal --mtu 1200 --nocomp --dscp 46 --parityshard 0"
def formattedAddress: String = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
def nameIsEmpty: Boolean = name == null || name.isEmpty
def getName: String = if (nameIsEmpty) formattedAddress else name
override def toString: String = "ss://" + Base64.encodeToString("%s%s:%s@%s:%d".formatLocal(Locale.ENGLISH, override def toString: String = "ss://" + Base64.encodeToString("%s%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
method, if (auth) "-auth" else "", password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP) + method, if (auth) "-auth" else "", password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP) +
'#' + URLEncoder.encode(name, "utf-8") '#' + URLEncoder.encode(name, "utf-8")
def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method) def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method)
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
.putString(Key.host, host)
.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort)
.putString(Key.password, password)
.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns)
.putString(Key.method, method)
.putBoolean(Key.proxyApps, proxyApps)
.putBoolean(Key.bypass, bypass)
.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.auth, auth)
.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual)
.putBoolean(Key.kcp, kcp)
.putInt(Key.kcpPort, kcpPort)
.putString(Key.kcpcli, kcpcli)
.remove(Key.dirty)
def deserialize(pref: SharedPreferences) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = pref.getString(Key.name, null)
host = pref.getString(Key.host, null)
localPort = pref.getInt(Key.localPort, 0)
remotePort = pref.getInt(Key.remotePort, 0)
password = pref.getString(Key.password, null)
method = pref.getString(Key.method, null)
route = pref.getString(Key.route, null)
remoteDns = pref.getString(Key.remoteDns, null)
proxyApps = pref.getBoolean(Key.proxyApps, false)
bypass = pref.getBoolean(Key.bypass, false)
udpdns = pref.getBoolean(Key.udpdns, false)
auth = pref.getBoolean(Key.auth, false)
ipv6 = pref.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null)
kcp = pref.getBoolean(Key.kcp, false)
kcpPort = pref.getInt(Key.kcpPort, 0)
kcpcli = pref.getString(Key.kcpcli, null)
}
} }
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.util.Log import android.util.Log
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
object ProfileManager { object ProfileManager {
...@@ -30,9 +31,6 @@ object ProfileManager { ...@@ -30,9 +31,6 @@ object ProfileManager {
class ProfileManager(dbHelper: DBHelper) { class ProfileManager(dbHelper: DBHelper) {
import ProfileManager._ import ProfileManager._
var profileAddedListener: Profile => Any = _
def setProfileAddedListener(listener: Profile => Any): Unit = this.profileAddedListener = listener
def createProfile(p: Profile = null): Profile = { def createProfile(p: Profile = null): Profile = {
val profile = if (p == null) new Profile else p val profile = if (p == null) new Profile else p
profile.id = 0 profile.id = 0
...@@ -52,7 +50,7 @@ class ProfileManager(dbHelper: DBHelper) { ...@@ -52,7 +50,7 @@ class ProfileManager(dbHelper: DBHelper) {
.prepareStatementString).getFirstResult .prepareStatementString).getFirstResult
if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1 if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1
dbHelper.profileDao.createOrUpdate(profile) dbHelper.profileDao.createOrUpdate(profile)
if (profileAddedListener != null) profileAddedListener(profile) if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.add(profile)
} catch { } catch {
case ex: Exception => case ex: Exception =>
Log.e(TAG, "addProfile", ex) Log.e(TAG, "addProfile", ex)
...@@ -90,6 +88,7 @@ class ProfileManager(dbHelper: DBHelper) { ...@@ -90,6 +88,7 @@ class ProfileManager(dbHelper: DBHelper) {
def delProfile(id: Int): Boolean = { def delProfile(id: Int): Boolean = {
try { try {
dbHelper.profileDao.deleteById(id) dbHelper.profileDao.deleteById(id)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.removeId(id)
true true
} catch { } catch {
case ex: Exception => case ex: Exception =>
...@@ -122,14 +121,4 @@ class ProfileManager(dbHelper: DBHelper) { ...@@ -122,14 +121,4 @@ class ProfileManager(dbHelper: DBHelper) {
None None
} }
} }
def createDefault(): Profile = {
val profile = new Profile {
name = "Default"
host = "198.199.101.152"
remotePort = 443
password = "u1rRWTssNv0p"
}
createProfile(profile)
}
} }
...@@ -151,28 +151,33 @@ object Key { ...@@ -151,28 +151,33 @@ object Key {
val localPort = "localPortNum" val localPort = "localPortNum"
val remoteDns = "remoteDns" val remoteDns = "remoteDns"
val profileTip = "profileTip"
val kcp = "kcp" val kcp = "kcp"
val kcpPort = "kcpPort" val kcpPort = "kcpPort"
val kcpcli = "kcpcli" val kcpcli = "kcpcli"
val dirty = "profileDirty"
val tfo = "tcp_fastopen" val tfo = "tcp_fastopen"
val currentVersionCode = "currentVersionCode" val currentVersionCode = "currentVersionCode"
} }
object State { object State {
/**
* This state will never be broadcast by the service. This state is only used to indicate that the current context
* hasn't bound to any context.
*/
val IDLE = 0
val CONNECTING = 1 val CONNECTING = 1
val CONNECTED = 2 val CONNECTED = 2
val STOPPING = 3 val STOPPING = 3
val STOPPED = 4 val STOPPED = 4
def isAvailable(state: Int): Boolean = state != CONNECTED && state != CONNECTING
} }
object Action { object Action {
val SERVICE = "com.github.shadowsocks.SERVICE" final val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE" final val CLOSE = "com.github.shadowsocks.CLOSE"
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
} }
object Route { object Route {
......
...@@ -38,8 +38,7 @@ object Parser { ...@@ -38,8 +38,7 @@ object Parser {
profile.method = ss.group(2).toLowerCase profile.method = ss.group(2).toLowerCase
if (ss.group(3) != null) profile.auth = true if (ss.group(3) != null) profile.auth = true
profile.password = ss.group(4) profile.password = ss.group(4)
profile.name = ss.group(5) profile.host = ss.group(5)
profile.host = profile.name
profile.remotePort = ss.group(6).toInt profile.remotePort = ss.group(6).toInt
if (m.group(2) != null) profile.name = URLDecoder.decode(m.group(3), "utf-8") if (m.group(2) != null) profile.name = URLDecoder.decode(m.group(3), "utf-8")
profile profile
......
...@@ -46,7 +46,7 @@ class TaskerSettings(bundle: Bundle) { ...@@ -46,7 +46,7 @@ class TaskerSettings(bundle: Bundle) {
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId) if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB, new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
app.profileManager.getProfile(profileId) match { app.profileManager.getProfile(profileId) match {
case Some(p) => context.getString(if (switchOn) R.string.start_service else R.string.stop_service, p.name) case Some(p) => context.getString(if (switchOn) R.string.start_service else R.string.stop_service, p.getName)
case None => context.getString(if (switchOn) R.string.start_service_default else R.string.stop) case None => context.getString(if (switchOn) R.string.start_service_default else R.string.stop)
}) })
} }
......
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.utils
import java.util
import android.content.Context
import android.graphics.Typeface
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
if (!cache.containsKey(assetPath)) {
try {
cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath))
} catch {
case e: Exception =>
Log.e("Typefaces", "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
private final val cache = new util.Hashtable[String, Typeface]
}
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.support.v7.widget.CardView
import android.util.AttributeSet
/**
* @author Mygod
*/
class BoundedCardView(context: Context, attrs: AttributeSet) extends CardView(context, attrs) with BoundedView {
initAttrs(context, attrs)
}
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* 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/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.support.v7.widget.GridLayout
import android.util.AttributeSet
/**
* @author Mygod
*/
class BoundedGridLayout(context: Context, attrs: AttributeSet) extends GridLayout(context, attrs) with BoundedView {
initAttrs(context, attrs)
}
...@@ -20,66 +20,33 @@ ...@@ -20,66 +20,33 @@
package com.github.shadowsocks.widget package com.github.shadowsocks.widget
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.CoordinatorLayout.Behavior
import android.support.design.widget.Snackbar.SnackbarLayout
import android.support.v4.view.animation.FastOutSlowInInterpolator
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import com.github.clans.fab.FloatingActionMenu import android.view.View.MeasureSpec
import com.github.shadowsocks.R
import scala.collection.JavaConverters._
/** /**
* Behavior for com.github.clans.fab.FloatingActionMenu that is aware of Snackbars and scrolling. * Based on: http://stackoverflow.com/a/6212120/2245107
* *
* @author Mygod * @author Mygod
*/ */
class FloatingActionMenuBehavior(context: Context, attrs: AttributeSet) trait BoundedView extends View {
extends Behavior[FloatingActionMenu](context, attrs) { private var boundedWidth: Int = _
private var fabTranslationYAnimator: ValueAnimator = _ private var boundedHeight: Int = _
private var fabTranslationY: Float = _
override def layoutDependsOn(parent: CoordinatorLayout, child: FloatingActionMenu, dependency: View): Boolean =
dependency.isInstanceOf[SnackbarLayout]
override def onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionMenu, protected def initAttrs(context: Context, attrs: AttributeSet) {
dependency: View): Boolean = { val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedView)
dependency match { boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0)
case _: SnackbarLayout => boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0)
var targetTransY = parent.getDependencies(child).asScala arr.recycle()
.filter(view => view.isInstanceOf[SnackbarLayout] && parent.doViewsOverlap(child, view))
.map(view => view.getTranslationY - view.getHeight).reduceOption(_ min _).getOrElse(0F)
if (targetTransY > 0) targetTransY = 0
if (fabTranslationY != targetTransY) {
val currentTransY = child.getTranslationY
if (fabTranslationYAnimator != null && fabTranslationYAnimator.isRunning) fabTranslationYAnimator.cancel()
if (child.isShown && Math.abs(currentTransY - targetTransY) > child.getHeight * 0.667F) {
if (fabTranslationYAnimator == null) {
fabTranslationYAnimator = new ValueAnimator
fabTranslationYAnimator.setInterpolator(new FastOutSlowInInterpolator)
fabTranslationYAnimator.addUpdateListener(animation =>
child.setTranslationY(animation.getAnimatedValue.asInstanceOf[Float]))
}
fabTranslationYAnimator.setFloatValues(currentTransY, targetTransY)
fabTranslationYAnimator.start()
} else child.setTranslationY(targetTransY)
fabTranslationY = targetTransY
}
}
false
} }
override def onStartNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, directTargetChild: View, override def onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int): Unit = super.onMeasure(
target: View, nestedScrollAxes: Int) = true if (boundedWidth > 0 && boundedWidth < MeasureSpec.getSize(widthMeasureSpec))
override def onNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, target: View, dxConsumed: Int, MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthMeasureSpec))
dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) { else widthMeasureSpec,
super.onNestedScroll(parent, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) if (boundedHeight > 0 && boundedHeight < MeasureSpec.getSize(heightMeasureSpec))
val dy = dyConsumed + dyUnconsumed MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightMeasureSpec))
if (child.isMenuButtonHidden) { else heightMeasureSpec)
if (dy < 0) child.showMenuButton(true)
} else if (dy > 0) child.hideMenuButton(true)
}
} }
...@@ -22,6 +22,7 @@ package com.github.shadowsocks.widget ...@@ -22,6 +22,7 @@ package com.github.shadowsocks.widget
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.view.View import android.view.View
import com.github.shadowsocks.R import com.github.shadowsocks.R
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment