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 ++=
"-keep class com.github.shadowsocks.System { *; }" ::
"-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" ::
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" ::
Nil
......@@ -31,17 +32,22 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.android.support" % "cardview-v7" % "25.1.0" ::
"com.android.support" % "design" % "25.1.0" ::
"com.android.support" % "gridlayout-v7" % "25.1.0" ::
"com.android.support" % "preference-v14" % "25.1.0" ::
"com.evernote" % "android-job" % "1.1.3" ::
"com.github.clans" % "fab" % "1.6.4" ::
"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-analytics" % "10.0.1" ::
"com.google.android.gms" % "play-services-gcm" % "10.0.1" ::
"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" ::
"dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
......
......@@ -38,7 +38,7 @@
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<activity
android:name=".Shadowsocks"
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
......@@ -49,28 +49,6 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</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>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
......@@ -82,18 +60,33 @@
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</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
android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".ProfileManagerActivity"
android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".Shadowsocks"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
......@@ -104,18 +97,6 @@
</intent-filter>
</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
android:name=".QuickToggleShortcut"
android:label="@string/quick_toggle"
......
......@@ -7,6 +7,8 @@ interface IShadowsocksService {
String getProfileName();
oneway void registerCallback(IShadowsocksServiceCallback cb);
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
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 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
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"/>
<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"/>
android:fillColor="#FFFFFFFF"
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"/>
</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 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>
......@@ -5,5 +5,5 @@
android:viewportHeight="24.0">
<path
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>
......@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<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"/>
</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 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<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"/>
</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>
......@@ -3,87 +3,89 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent"
tools:ignore="UnknownIdInLayout" android:clipChildren="false">
<LinearLayout android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_below="@+id/toolbar"
android:animateLayoutChanges="true">
<include layout="@layout/toolbar_light_dark" />
<LinearLayout android:id="@+id/stat" android:elevation="4dp" android:visibility="gone"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight" android:gravity="center_vertical"
android:paddingStart="16dip" android:paddingEnd="16dip" android:focusable="true"
android:paddingTop="16dip" android:paddingBottom="16dip"
android:background="@drawable/background_stat" android:orientation="vertical"
android:nextFocusRight="@+id/fab">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:text="@string/stat"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/connection_test"
android:gravity="end"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<android.support.v7.widget.GridLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="72dip"
app:columnCount="4">
<TextView app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:ellipsize="marquee" android:text="@string/sent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"/>
<TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill_horizontal"/>
<TextView app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee" android:text="▲"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
<TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"
android:ellipsize="marquee" android:text="@string/received"/>
<TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" />
<TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/>
<TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
</android.support.v7.widget.GridLayout>
</LinearLayout>
<FrameLayout android:id="@+id/fragment_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/stat"/>
<LinearLayout android:id="@+id/stat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:paddingStart="16dip"
android:paddingEnd="16dip"
android:paddingTop="16dip"
android:paddingBottom="16dip"
android:focusable="true"
android:background="@drawable/background_stat"
android:orientation="vertical"
android:nextFocusRight="@+id/fab"
android:layout_above="@+id/snackbar">
<TextView android:id="@+id/status"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:paddingEnd="72dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.github.shadowsocks.widget.BoundedGridLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
app:bounded_width="288dp"
app:columnCount="4">
<TextView app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:ellipsize="marquee" android:text="@string/sent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"/>
<TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill_horizontal"/>
<TextView app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee" android:text="▲"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
<TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"
android:ellipsize="marquee" android:text="@string/received"/>
<TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" />
<TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/>
<TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
</com.github.shadowsocks.widget.BoundedGridLayout>
</LinearLayout>
<fragment android:name="com.github.shadowsocks.ShadowsocksSettings"
android:id="@android:id/content" android:layout_below="@+id/title_bar"
android:layout_width="match_parent" android:layout_height="match_parent"/>
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle" app:reusable="true"
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle"
app:reusable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:elevation="6dp"
android:layout_alignBottom="@+id/title_bar"
android:layout_height="wrap_content"
android:elevation="6dp"
android:layout_alignTop="@+id/stat"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/fab_margin_bottom">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content"
android:layout_marginTop="@dimen/fab_margin_top">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/material_blue_grey_700"
app:backgroundTint="@color/material_primary_500"
app:srcCompat="@drawable/ic_start_idle"
app:pressedTranslationZ="6dp" app:borderWidth="0dp" />
app:pressedTranslationZ="6dp"
app:borderWidth="0dp"/>
</com.github.jorgecastilloprz.FABProgressCircle>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<?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"?>
<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:orientation="vertical"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<android.support.design.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="0dp" android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/menu"
android:layout_width="match_parent"
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>
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="32dp"
android:clipChildren="false"
android:clipToPadding="false"/>
</LinearLayout>
<?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 -->
<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_height="wrap_content"
android:id="@+id/container"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:focusable="true"
android:nextFocusRight="@+id/share">
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:gravity="center_vertical"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:maxLines="4"
android:scrollHorizontally="false" />
<ImageView android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_gravity="center_vertical"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share"
android:focusable="true"
android:nextFocusLeft="@+id/container"/>
</LinearLayout>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->
<FrameLayout 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_height="wrap_content">
<com.github.shadowsocks.widget.BoundedCardView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_selectable"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_horizontal"
app:bounded_width="@dimen/profile_item_max_width"
android:id="@+id/indicator">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:focusable="true"
android:nextFocusRight="@+id/edit"
android:id="@+id/content"
android:background="?android:attr/selectableItemBackground">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:id="@+id/container">
<TextView android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_vertical"
android:maxLines="2"
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"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
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"
android:title="@string/action_export"
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 @@
<string name="nat_no_root">NATモードはROOT権限が必要です</string>
<string name="switch_to_vpn">VPNモードに切り替える</string>
<string name="stop">中止</string>
<string name="quick_switch">切り替え</string>
<string name="stopping">停止中……</string>
<string name="vpn_error">バックグランドサービスの起動に失敗しました:%s</string>
<string name="reboot_required">VPNサービスの起動に失敗しました、デバイスの再起動を試みて下さい</string>
......@@ -52,7 +51,6 @@
<string name="settings">オプション設定</string>
<string name="remove_profile">このプロファイルを削除「%s」?</string>
<string name="recovery">リセット</string>
<string name="recovery_summary">すべてのバックグラウンドサービスをリセット</string>
<string name="about">本アプリについて</string>
<string name="about_title">シャドーソックス(shadowsocks)%s</string>
<string name="flush_dnscache">DNS キャッシュをリフレッシュ</string>
......@@ -60,16 +58,11 @@
<string name="flush_dnscache_no_root">ROOT権限がない場合はDNSキャッシュをリフレッシュできません</string>
<string name="flushing">リフレッシュ中……</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="nat">NAT モード (デバックのみ)</string>
<string name="nat_summary">VPNモードからNATモードに切り替えるにはROOT権限が必要です</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_summary">UDPプロトコルでリモードサーバーにパケットを転送</string>
......@@ -80,7 +73,6 @@
<string name="ipv6">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="connection_test_pending">接続状況確認</string>
......@@ -91,16 +83,10 @@
<string name="connection_test_error_status_code">ステータスコード無効(#%d)</string>
<!-- profile -->
<string name="profile_manager_dialog">プロファイル</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="copy_url">URL をコピー</string>
<string name="add_profile_dialog">シャドーソックスにプロファイルを追加しますか</string>
<string name="add_profile_methods_scan_qr_code">QRコードを読み取る</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-array name="route_entry">
......
......@@ -9,7 +9,6 @@
<string name="profile_summary">Переключить на другой профиль или добавить новые</string>
<string name="nat">Режим NAT (не рекомендуется)</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_profiles">\n%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
......@@ -74,7 +73,6 @@
<string name="stopping">Останавливается…</string>
<string name="vpn_error">%s</string>
<string name="reboot_required">Сбой запуска VPN сервиса. Требуется перезагрузка Вашего устройства.</string>
<string name="quick_switch">Переключить</string>
<!-- alert category -->
<string name="close">Закрыть</string>
......@@ -89,14 +87,9 @@
<string name="profiles">Профили</string>
<string name="settings">Настройки</string>
<string name="recovery">Сброс</string>
<string name="recovery_summary">Сброс всех фоновых задач</string>
<string name="about">О приложении</string>
<string name="about_title">Shadowsocks %s</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_summary">Работает только в режиме NAT. Требует наличия ROOT прав.</string>
<string name="flush_dnscache_no_root">Очистка кэша DNS не работает без налиичя ROOT прав</string>
......@@ -112,15 +105,9 @@
<string name="action_import_err">Сбой импорта.</string>
<!-- 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_methods_scan_qr_code">Сканировать QR код</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="copy_url">Копировать URL</string>
<plurals name="removed">
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin_bottom">-28dp</dimen>
<dimen name="fab_margin_top">-28dp</dimen>
</resources>
......@@ -38,11 +38,13 @@
<string name="nat_no_root">NAT 模式需要 ROOT 权限</string>
<string name="switch_to_vpn">切换到 VPN 模式</string>
<string name="stop">停止</string>
<string name="quick_switch">切换</string>
<string name="stopping">正在关闭……</string>
<string name="vpn_error">后台服务启动失败:%s</string>
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
<string name="yes"></string>
<string name="no"></string>
<string name="close">关闭</string>
<string name="profile_empty">请选择配置文件</string>
<string name="proxy_empty">代理服务器地址及密码不能为空</string>
<string name="connect">连接</string>
<string name="initializing">正在初始化……</string>
......@@ -52,24 +54,19 @@
<string name="settings">设置选项</string>
<string name="remove_profile">删除此配置文件「%s」?</string>
<string name="recovery">重置</string>
<string name="recovery_summary">重置所有后台服务</string>
<string name="about">关于</string>
<string name="about_title">影梭(shadowsocks)%s</string>
<string name="flush_dnscache">刷新 DNS 缓存</string>
<string name="flush_dnscache_summary">只需在 NAT 模式下使用,需要 ROOT 权限</string>
<string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string>
<string name="flushing">刷新中……</string>
<string name="edit">编辑</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="nat">NAT 模式 (仅限调试)</string>
<string name="nat_summary">从 VPN 模式切换为 NAT 模式,需要 ROOT 权限</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_summary">由远程服务器转发 UDP 协议的数据包</string>
......@@ -80,7 +77,6 @@
<string name="ipv6">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="connection_test_pending">检查网络连接</string>
......@@ -91,16 +87,16 @@
<string name="connection_test_error_status_code">状态码无效(#%d)</string>
<!-- profile -->
<string name="profile_manager_dialog">配置文件</string>
<string name="profile_manager_dialog_content">&#8226; 点击选择配置文件;\n&#8226; 向左/右滑动或按左删除;\n&#8226;
长按拖动改变顺序。</string>
<string name="gotcha">好的</string>
<string name="profile_config">配置文件设置</string>
<string name="unsaved_changes_prompt">保存修改吗?</string>
<string name="apply">应用</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="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</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-array name="route_entry">
......@@ -134,6 +130,10 @@
<!-- status -->
<string name="sent">发送:</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 -->
<string name="kcp_summary">启用 KCP 协议</string>
......
......@@ -39,11 +39,13 @@
<string name="nat_no_root">NAT 模式需要 ROOT 權限</string>
<string name="switch_to_vpn">切換至 VPN 模式</string>
<string name="stop">停止</string>
<string name="quick_switch">切換</string>
<string name="stopping">關閉中……</string>
<string name="vpn_error">%s</string>
<string name="reboot_required">VPN 服務啟動失敗。您或許需要重新啟動您的裝置。</string>
<string name="yes"></string>
<string name="no"></string>
<string name="close">關閉</string>
<string name="profile_empty">請選擇設定檔</string>
<string name="proxy_empty">Proxy 或密碼不可以空白</string>
<string name="connect">連線</string>
<string name="initializing">起始中……</string>
......@@ -53,24 +55,19 @@
<string name="settings">設定</string>
<string name="remove_profile">移除此設定檔 "%s"?</string>
<string name="recovery">重設</string>
<string name="recovery_summary">重設一切背景服務</string>
<string name="about">關於</string>
<string name="about_title">Shadowsocks %s</string>
<string name="flush_dnscache">清除 DNS 快取</string>
<string name="flush_dnscache_summary">僅在 NAT 模式可用,需要 ROOT 權限</string>
<string name="flush_dnscache_no_root">在無 ROOT 權限時清除 DNS 快取無法進行</string>
<string name="flushing">清除中……</string>
<string name="edit">編輯</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="nat">NAT 模式 (已過時)</string>
<string name="nat_summary">使用 NAT 模式代替 VPN 模式,需要 ROOT 權限</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_summary">向遠端轉送 UDP 封包</string>
......@@ -81,7 +78,6 @@
<string name="ipv6">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="connection_test_pending">檢查連線能力</string>
......@@ -92,17 +88,16 @@
<string name="connection_test_error_status_code">錯誤碼: #%d</string>
<!-- profile -->
<string name="profile_manager_dialog">設定檔</string>
<string name="profile_manager_dialog_content">&#8226; 輕觸選擇一個設定檔;\n&#8226; 向左/右撥動或按左移除;\n&#8226;
按住後上下撥動設定檔重新排序。</string>
<string name="gotcha">我懂了!</string>
<string name="profile_config">設定檔配置</string>
<string name="unsaved_changes_prompt">要保存更改嗎?</string>
<string name="apply">應用</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="add_profile_dialog">為 Shadowsocks 新增此設定檔?</string>
<string name="add_profile_methods_scan_qr_code">掃描 QR 碼</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-array name="route_entry">
......@@ -136,6 +131,10 @@
<!-- status -->
<string name="sent">傳送:</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 -->
<string name="kcp_summary">啟用 KCP 協定</string>
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BoundedView">
<attr name="bounded_width" format="dimension" />
<attr name="bounded_height" format="dimension" />
</declare-styleable>
<declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" />
<attr name="min" format="integer" />
......
......@@ -3,12 +3,13 @@
<resources>
<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_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</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_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>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="profile_item_max_width">480dp</dimen>
<!-- 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>
......@@ -11,10 +11,8 @@
<string name="nat">NAT mode (deprecated)</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_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_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>
<plurals name="bytes" translatable="false">
<item quantity="one">Byte</item>
......@@ -82,10 +80,13 @@
<string name="stopping">Shutting down…</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="quick_switch">Switch</string>
<string name="profile_invalid_input">No valid profile data found.</string>
<!-- alert category -->
<string name="yes">Yes</string>
<string name="no">No</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="connect">Connect</string>
<string name="initializing">Initializing…</string>
......@@ -97,14 +98,10 @@
<string name="profiles">Profiles</string>
<string name="settings">Settings</string>
<string name="recovery">Reset</string>
<string name="recovery_summary">Reset all the background services</string>
<string name="about">About</string>
<string name="about_title">Shadowsocks %s</string>
<string name="edit">Edit</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_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>
......@@ -119,15 +116,15 @@
<string name="action_import_err">Failed to import.</string>
<!-- profile -->
<string name="profile_manager_dialog">Profiles</string>
<string name="profile_manager_dialog_content">&#8226; Tap to select a profile;\n&#8226; Swipe left/right or press left
to remove;\n&#8226; Long press to reorder profiles.</string>
<string name="gotcha">Gotcha!</string>
<string name="profile_config">Profile config</string>
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
<string name="apply">Apply</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_methods_scan_qr_code">Scan QR code</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="copy_url">Copy URL</string>
<plurals name="removed">
......@@ -146,6 +143,10 @@
<!-- status -->
<string name="sent">Sent:</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 -->
<string name="kcp" translatable="false">KCP</string>
......
......@@ -6,14 +6,16 @@
<item name="android:textSize">24sp</item>
</style>
<style name="Theme.MaterialBase" parent="Theme.AppCompat.Light.NoActionBar">
<style name="Theme.MaterialBase" parent="MaterialDrawerTheme.Light.DarkToolbar">
<!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="colorAccent">@color/material_primary_500</item>
<item name="colorButtonNormal">@color/material_primary_500</item>
<item name="colorAccent">@color/material_accent_200</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
<item name="material_drawer_selected_text">@color/material_accent_200</item>
</style>
<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 @@
<be.mygod.preference.EditTextPreference
android:key="profileName"
android:persistent="false"
android:summary="%s"
android:title="@string/profile_name"/>
<be.mygod.preference.EditTextPreference
android:key="proxy"
android:persistent="false"
android:summary="%s"
android:title="@string/proxy"/>
<be.mygod.preference.NumberPickerPreference
app:min="1"
app:max="65535"
android:key="remotePortNum"
android:persistent="false"
android:summary="%d"
android:title="@string/remote_port"/>
<be.mygod.preference.NumberPickerPreference
app:min="1025"
app:max="65535"
android:key="localPortNum"
android:persistent="false"
android:summary="%d"
android:title="@string/port"/>
<be.mygod.preference.EditTextPreference
android:inputType="textPassword"
android:key="sitekey"
android:persistent="false"
android:summary="%s"
android:title="@string/sitekey"/>
<DropDownPreference
android:key="encMethod"
android:persistent="false"
android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
<SwitchPreference
android:key="isAuth"
android:persistent="false"
android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/>
......@@ -55,7 +48,6 @@
<DropDownPreference
android:key="route"
android:persistent="false"
android:entries="@array/route_entry"
android:entryValues="@array/route_value"
android:summary="%s"
......@@ -63,20 +55,17 @@
<be.mygod.preference.EditTextPreference
android:key="remoteDns"
android:title="@string/remote_dns"
android:summary="@string/remote_dns_summary"/>
android:summary="%s"/>
<SwitchPreference
android:key="isIpv6"
android:persistent="false"
android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/>
<SwitchPreference
android:key="isProxyApps"
android:persistent="false"
android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/>
<SwitchPreference
android:key="isUdpDns"
android:persistent="false"
android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/>
......@@ -87,7 +76,6 @@
<SwitchPreference
android:key="kcp"
android:persistent="false"
android:summary="@string/kcp_summary"
android:title="@string/kcp"/>
......@@ -95,41 +83,14 @@
app:min="1"
app:max="65535"
android:key="kcpPort"
android:persistent="false"
android:summary="@string/kcp_port_summary"
android:title="@string/kcp_port"/>
<be.mygod.preference.EditTextPreference
android:key="kcpcli"
android:persistent="false"
android:summary="%s"
android:title="@string/kcpcli"/>
</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>
......@@ -8,14 +8,6 @@
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.QuickToggleShortcut" />
</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"
android:icon="@drawable/ic_qu_camera_launcher"
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 {
proxiedApps.add(item.packageName)
check.setChecked(true)
}
if (!appsLoading.get) {
profile.individual = proxiedApps.mkString("\n")
app.profileManager.updateProfile(profile)
}
if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
}
}
......@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private var loadingView: View = _
private val appsLoading = new AtomicBoolean
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() {
instance = null
......@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = profile.individual
val proxiedAppString = app.settings.getString(Key.individual, null)
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
......@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
}
return true
case R.id.action_export =>
val bypass = profile.bypass
val proxiedAppString = profile.individual
val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
......@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
profile.individual = apps
app.profileManager.updateProfile(profile)
app.editor.putString(Key.individual, apps).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
......@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
if (profile == null) finish()
handler = new Handler()
this.setContentView(R.layout.layout_apps)
......@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!profile.proxyApps) {
profile.proxyApps = true
app.profileManager.updateProfile(profile)
}
if (!app.settings.getBoolean(Key.proxyApps, false)) app.editor.putBoolean(Key.proxyApps, true).apply()
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
profile.proxyApps = checked
app.profileManager.updateProfile(profile)
app.editor.putBoolean(Key.proxyApps, checked).apply()
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(profile.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
profile.bypass = checked
app.profileManager.updateProfile(profile)
})
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) => app.editor.putBoolean(Key.bypass, checked).apply())
initProxiedApps()
loadingView = findViewById(R.id.loading)
......
......@@ -21,20 +21,26 @@
package com.github.shadowsocks
import java.io.IOException
import java.net.InetAddress
import java.util
import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask}
import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.net.ConnectivityManager
import android.os.{Handler, RemoteCallbackList}
import android.os.{Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.github.kevinsawicki.http.HttpRequest
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import okhttp3.{Dns, FormBody, OkHttpClient, Request}
import scala.collection.mutable
import scala.util.Random
trait BaseService extends Service {
......@@ -49,7 +55,7 @@ trait BaseService extends Service {
var trafficMonitorThread: TrafficMonitorThread = _
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 restartHanlder = new Handler(getMainLooper)
lazy val protectPath: String = getApplicationInfo.dataDir + "/protect_path"
......@@ -78,38 +84,36 @@ trait BaseService extends Service {
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) {
if (cb != null && callbacks.unregister(cb)) {
callbacksCount -= 1
if (callbacksCount == 0 && timer != null) {
timer.cancel()
timer = null
}
}
}
override def registerCallback(cb: IShadowsocksServiceCallback): Unit = callbacks.register(cb)
override def registerCallback(cb: IShadowsocksServiceCallback) {
if (cb != null && callbacks.register(cb)) {
callbacksCount += 1
if (callbacksCount != 0 && timer == null) {
val task = new TimerTask {
def run() {
if (TrafficMonitor.updateRate()) updateTrafficRate()
}
}
override def startListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
if (bandwidthListeners.add(cb.asBinder)){
if (timer == null) {
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()
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 {
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.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
stopRunner(stopService = false)
......@@ -132,13 +136,28 @@ trait BaseService extends Service {
val container = holder.getContainer
val url = container.getString("proxy_url")
val sig = Utils.getSignature(this)
val list = HttpRequest
.post(url)
.connectTimeout(2000)
.readTimeout(2000)
.send("sig="+sig)
.body
val proxies = util.Random.shuffle(list.split('|').toSeq)
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
case _ => Dns.SYSTEM.lookup(hostname)
})
.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(':')
profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt
......@@ -238,7 +257,7 @@ trait BaseService extends Service {
def updateTrafficRate() {
handler.post(() => {
if (callbacksCount > 0) {
if (bandwidthListeners.nonEmpty) {
val txRate = TrafficMonitor.txRate
val rxRate = TrafficMonitor.rxRate
val txTotal = TrafficMonitor.txTotal
......@@ -246,7 +265,8 @@ trait BaseService extends Service {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
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 {
case _: Exception => // Ignore
}
......@@ -259,7 +279,7 @@ trait BaseService extends Service {
override def onCreate() {
super.onCreate()
app.refreshContainerHolder
app.refreshContainerHolder()
app.updateAssets()
}
......@@ -268,8 +288,8 @@ trait BaseService extends Service {
protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper)
handler.post(() => if (state != s || msg != null) {
if (callbacksCount > 0) {
if (state != s || msg != null) {
if (callbacks.getRegisteredCallbackCount > 0) handler.post(() => {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
......@@ -279,9 +299,9 @@ trait BaseService extends Service {
}
}
callbacks.finishBroadcast()
}
})
state = s
})
}
}
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
}
}
}
......@@ -22,244 +22,228 @@ package com.github.shadowsocks
import java.lang.System.currentTimeMillis
import java.net.{HttpURLConnection, URL}
import java.util
import java.util.{GregorianCalendar, Locale}
import java.util.Locale
import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.graphics.Typeface
import android.net.VpnService
import android.os._
import android.nfc.{NdefMessage, NfcAdapter}
import android.os.{Build, Bundle, Handler, Message}
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.app.AlertDialog
import android.support.v7.content.res.AppCompatResources
import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log
import android.view.{View, ViewGroup}
import android.widget._
import android.view.View
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database._
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import scala.util.Random
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(TAG, "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
import com.mikepenz.crossfader.Crossfader
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
import com.mikepenz.materialdrawer.interfaces.ICrossfader
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.{Drawer, DrawerBuilder}
object MainActivity {
private final val TAG = "ShadowsocksMainActivity"
private final val REQUEST_CONNECT = 1
private final val TAG = "Typefaces"
private final val cache = new util.Hashtable[String, Typeface]
private final val DRAWER_PROFILES = 0L
private final val DRAWER_GLOBAL_SETTINGS = 1L
private final val DRAWER_RECOVERY = 2L
private final val DRAWER_ABOUT = 3L
}
object Shadowsocks {
// Constants
private final val TAG = "Shadowsocks"
private final val REQUEST_CONNECT = 1
}
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnSharedPreferenceChangeListener {
import MainActivity._
class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
import Shadowsocks._
// UI
private val handler = new Handler()
private var fab: FloatingActionButton = _
private var fabProgressCircle: FABProgressCircle = _
var crossfader: Crossfader[CrossFadeSlidingPaneLayout] = _
var drawer: Drawer = _
// Variables
var serviceStarted: Boolean = _
var fab: FloatingActionButton = _
var fabProgressCircle: FABProgressCircle = _
var progressDialog: ProgressDialog = _
var state = State.STOPPED
var currentProfile = new Profile
private var testCount: Int = _
private var statusText: TextView = _
private var txText: TextView = _
private var rxText: TextView = _
private var txRateText: TextView = _
private var rxRateText: TextView = _
private var currentFragment: ToolbarFragment = _
private lazy val profilesFragment = new ProfilesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
private lazy val aboutFragment = new AboutFragment()
// Services
var state: Int = _
private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, profileName: String, m: String) {
handler.post(() => {
s match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
fabProgressCircle.show()
preferences.setEnabled(false)
stat.setVisibility(View.GONE)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
if (state == State.CONNECTING) {
fabProgressCircle.beginFinalAnimation()
} else {
fabProgressCircle.postDelayed(hideCircle, 1000)
}
fab.setEnabled(true)
changeSwitch(checked = true)
preferences.setEnabled(false)
stat.setVisibility(View.VISIBLE)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
connectionTestText.setText(getString(R.string.connection_test_pending))
}
case State.STOPPED =>
fab.setBackgroundTintList(greyTint)
fabProgressCircle.postDelayed(hideCircle, 1000)
fab.setEnabled(true)
changeSwitch(checked = false)
if (m != null) {
val snackbar = Snackbar.make(findViewById(android.R.id.content),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn,
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
snackbar.show()
Log.e(TAG, "Error to start VPN service: " + m)
}
preferences.setEnabled(true)
stat.setVisibility(View.GONE)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
preferences.setEnabled(false)
stat.setVisibility(View.GONE)
def stateChanged(s: Int, profileName: String, m: String): Unit = handler.post(() => changeState(s, profileName, m))
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
}
private lazy val greyTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_primary_500)
private lazy val greenTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_green_700)
private def hideCircle() = try fabProgressCircle.hide() catch {
case _: NullPointerException =>
}
private def changeState(s: Int, profileName: String = null, m: String = null) {
s match {
case State.CONNECTING =>
fab.setImageResource(R.drawable.ic_start_busy)
fabProgressCircle.show()
statusText.setText(R.string.connecting)
case State.CONNECTED =>
if (state == State.CONNECTING) fabProgressCircle.beginFinalAnimation()
else fabProgressCircle.postDelayed(hideCircle, 1000)
fab.setImageResource(R.drawable.ic_start_connected)
statusText.setText(if (app.isNatEnabled) R.string.nat_connected else R.string.vpn_connected)
case State.STOPPING =>
fab.setImageResource(R.drawable.ic_start_busy)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
statusText.setText(R.string.stopping)
case _ =>
fab.setImageResource(R.drawable.ic_start_idle)
fabProgressCircle.postDelayed(hideCircle, 1000)
if (m != null) {
val snackbar = Snackbar.make(findViewById(R.id.snackbar),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
if (m == getString(R.string.nat_no_root)) addDisableNatToSnackbar(snackbar)
snackbar.show()
Log.e(TAG, "Error to start VPN service: " + m)
}
state = s
})
statusText.setText(R.string.not_connected)
}
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
state = s
if (state == State.CONNECTED) fab.setBackgroundTintList(greenTint) else {
fab.setBackgroundTintList(greyTint)
updateTraffic(0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
if (ProfilesFragment.instance != null) {
val adapter = ProfilesFragment.instance.profilesAdapter
adapter.notifyDataSetChanged() // refresh button enabled state
if (state == State.STOPPED) adapter.refreshId(app.profileId) // refresh bandwidth statistics
}
fab.setEnabled(false)
if (state == State.CONNECTED || state == State.STOPPED)
handler.postDelayed(() => fab.setEnabled(state == State.CONNECTED || state == State.STOPPED), 1000)
}
def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.setText(TrafficMonitor.formatTraffic(txTotal))
rxText.setText(TrafficMonitor.formatTraffic(rxTotal))
txRateText.setText(TrafficMonitor.formatTraffic(txRate) + "/s")
rxRateText.setText(TrafficMonitor.formatTraffic(rxRate) + "/s")
val child = getFragmentManager.findFragmentById(R.id.fragment_holder).asInstanceOf[ToolbarFragment]
if (state != State.STOPPING && child != null) child.onTrafficUpdated(txRate, rxRate, txTotal, rxTotal)
}
def attachServiceCallback(): Unit = attachService(callback)
override def onServiceConnected() {
// Update the UI
if (fab != null) fab.setEnabled(true)
updateState()
changeState(bgService.getState)
if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) {
val snackbar = Snackbar.make(findViewById(android.R.id.content), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
snackbar.setAction(R.string.switch_to_vpn, (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
val snackbar = Snackbar.make(findViewById(R.id.snackbar), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
addDisableNatToSnackbar(snackbar)
snackbar.show()
}
}
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
override def onServiceDisconnected() {
if (fab != null) fab.setEnabled(false)
}
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false)): View.OnClickListener)
override def binderDied() {
detachService()
app.crashRecovery()
attachServiceCallback()
}
private var testCount: Int = _
private var stat: View = _
private var connectionTestText: TextView = _
private var txText: TextView = _
private var rxText: TextView = _
private var txRateText: TextView = _
private var rxRateText: TextView = _
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
private var adView: AdView = _
private lazy val preferences =
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
val handler = new Handler()
private def changeSwitch(checked: Boolean) {
serviceStarted = checked
fab.setImageResource(if (checked) R.drawable.ic_start_connected else R.drawable.ic_start_idle)
if (fab.isEnabled) {
fab.setEnabled(false)
handler.postDelayed(() => fab.setEnabled(true), 1000)
}
attachService(callback)
}
private def showProgress(msg: Int): Handler = {
clearDialog()
progressDialog = ProgressDialog.show(this, "", getString(msg), true, false)
new Handler {
override def handleMessage(msg: Message) {
clearDialog()
}
}
}
def cancelStart() {
clearDialog()
changeSwitch(checked = false)
}
def prepareStartService() {
Utils.ThrowableFuture {
if (app.isNatEnabled) serviceLoad() else {
val intent = VpnService.prepare(this)
if (intent != null) {
startActivityForResult(intent, REQUEST_CONNECT)
} else {
handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
}
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => bgService.use(app.profileId)
case _ => Log.e(TAG, "Failed to start VpnService")
}
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
// Initialize Toolbar
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle("shadowsocks") // non-translatable logo
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
val field = classOf[Toolbar].getDeclaredField("mTitleTextView")
field.setAccessible(true)
val title = field.get(toolbar).asInstanceOf[TextView]
title.setFocusable(true)
title.setGravity(0x10)
title.getLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
title.setOnClickListener(_ => startActivity(new Intent(this, classOf[ProfileManagerActivity])))
val typedArray = obtainStyledAttributes(Array(R.attr.selectableItemBackgroundBorderless))
title.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
val drawerBuilder = new DrawerBuilder()
.withActivity(this)
.withTranslucentStatusBar(true)
.withHeader(R.layout.layout_header)
.addDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_PROFILES)
.withName(R.string.profiles)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
.withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_settings))
.withIconTintingEnabled(true)
)
.addStickyDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_RECOVERY)
.withName(R.string.recovery)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_navigation_refresh))
.withIconTintingEnabled(true)
.withSelectable(false),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_ABOUT)
.withName(R.string.about)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_copyright))
.withIconTintingEnabled(true)
)
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.withSavedInstance(savedInstanceState)
val miniDrawerWidth = getResources.getDimension(R.dimen.material_mini_drawer_item)
if (getResources.getDisplayMetrics.widthPixels >=
getResources.getDimension(R.dimen.profile_item_max_width) + miniDrawerWidth) {
drawer = drawerBuilder.withGenerateMiniDrawer(true).buildView()
crossfader = new Crossfader[CrossFadeSlidingPaneLayout]()
crossfader.withContent(findViewById(android.R.id.content))
.withFirst(drawer.getSlider, getResources.getDimensionPixelSize(R.dimen.material_drawer_width))
.withSecond(drawer.getMiniDrawer.build(this), miniDrawerWidth.toInt)
.withSavedInstance(savedInstanceState)
.build()
if (getResources.getConfiguration.getLayoutDirection == View.LAYOUT_DIRECTION_RTL)
crossfader.getCrossFadeSlidingPaneLayout.setShadowDrawableRight(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_right))
else crossfader.getCrossFadeSlidingPaneLayout.setShadowDrawableLeft(
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_left))
drawer.getMiniDrawer.withCrossFader(new ICrossfader { // a wrapper is needed
def isCrossfaded: Boolean = crossfader.isCrossFaded
def crossfade(): Unit = crossfader.crossFade()
})
} else drawer = drawerBuilder.build()
val header = drawer.getHeader
val title = header.findViewById(R.id.drawer_title).asInstanceOf[TextView]
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
title.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_arrow_drop_down, 0)
stat = findViewById(R.id.stat)
connectionTestText = findViewById(R.id.connection_test).asInstanceOf[TextView]
if (savedInstanceState == null) displayFragment(profilesFragment)
statusText = findViewById(R.id.status).asInstanceOf[TextView]
txText = findViewById(R.id.tx).asInstanceOf[TextView]
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView]
stat.setOnClickListener(_ => {
val id = synchronized {
testCount += 1
handler.post(() => connectionTestText.setText(R.string.connection_test_testing))
testCount
}
findViewById(R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
testCount += 1
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
Utils.ThrowableFuture {
// Based on: https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/connectivity/NetworkMonitor.java#640
autoDisconnect(new URL("https", "www.google.com", "/generate_204").openConnection()
......@@ -284,11 +268,10 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
success = false
result = getString(R.string.connection_test_error, e.getMessage)
}
synchronized(if (testCount == id && app.isVpnEnabled) handler.post(() =>
if (success) connectionTestText.setText(result) else {
connectionTestText.setText(R.string.connection_test_fail)
Snackbar.make(findViewById(android.R.id.content), result, Snackbar.LENGTH_LONG).show()
}))
if (testCount == id) handler.post(() => if (success) statusText.setText(result) else {
statusText.setText(R.string.connection_test_fail)
Snackbar.make(findViewById(R.id.snackbar), result, Snackbar.LENGTH_LONG).show()
})
}
}
}
......@@ -296,171 +279,126 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (serviceStarted) serviceStop()
else if (bgService != null) prepareStartService()
else changeSwitch(checked = false))
fab.setOnLongClickListener((v: View) => {
Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect,
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
if (app.isNatEnabled) bgService.use(app.profileId) else {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
}
})
fab.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(this, if (state == State.CONNECTED) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show()
true
})
updateTraffic(0, 0, 0, 0)
handler.post(attachServiceCallback)
changeState(State.IDLE) // reset everything to init state
handler.post(() => attachService(callback))
app.settings.registerOnSharedPreferenceChangeListener(this)
val intent = getIntent
if (intent != null) handleShareIntent(intent)
}
private def hideCircle() {
try {
fabProgressCircle.hide()
} catch {
case _: java.lang.NullPointerException => // Ignore
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
private def updateState(resetConnectionTest: Boolean = true) {
if (bgService != null) {
bgService.getState match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show()
stat.setVisibility(View.GONE)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
serviceStarted = true
fab.setImageResource(R.drawable.ic_start_connected)
preferences.setEnabled(false)
fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.VISIBLE)
if (resetConnectionTest || state != State.CONNECTED)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
connectionTestText.setText(getString(R.string.connection_test_pending))
}
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show()
stat.setVisibility(View.GONE)
case _ =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_idle)
preferences.setEnabled(true)
fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.GONE)
}
state = bgService.getState
def handleShareIntent(intent: Intent) {
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
Snackbar.make(findViewById(R.id.snackbar), R.string.profile_invalid_input, Snackbar.LENGTH_LONG).show()
return
}
new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, null)
.setMessage(profiles.mkString("\n"))
.create()
.show()
}
private def updateCurrentProfile() = {
// Check if current profile changed
if (preferences.profile == null || app.profileId != preferences.profile.id) {
updatePreferenceScreen(app.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
app.switchProfile((app.profileManager.getFirstProfile match {
case Some(first) => first
case None => app.profileManager.createDefault()
}).id)
})
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => handler.post(() => {
detachService()
attachService(callback)
})
case _ =>
}
if (serviceStarted) serviceLoad()
private def displayFragment(fragment: ToolbarFragment) {
currentFragment = fragment
getFragmentManager.beginTransaction().replace(R.id.fragment_holder, fragment).commitAllowingStateLoss()
drawer.closeDrawer()
}
true
} else {
preferences.refreshProfile()
false
override def onItemClick(view: View, position: Int, drawerItem: IDrawerItem[_, _ <: ViewHolder]): Boolean = {
drawerItem.getIdentifier match {
case DRAWER_PROFILES => displayFragment(profilesFragment)
case DRAWER_RECOVERY =>
app.track("GlobalConfigFragment", "reset")
if (bgService != null) bgService.use(-1)
val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
val handler = new Handler {
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
}
Utils.ThrowableFuture {
app.copyAssets()
handler.sendEmptyMessage(0)
}
case DRAWER_GLOBAL_SETTINGS => displayFragment(globalSettingsFragment)
case DRAWER_ABOUT =>
app.track(TAG, "about")
displayFragment(aboutFragment)
}
true // unexpected cases will throw exception
}
protected override def onResume() {
super.onResume()
app.refreshContainerHolder
updateState(updateCurrentProfile())
}
private def updatePreferenceScreen(profile: Profile) {
if (profile.host == "198.199.101.152") if (adView == null) {
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1)
// Demographics
val random = new Random()
val adBuilder = new AdRequest.Builder()
adBuilder.setGender(AdRequest.GENDER_MALE)
val year = 1975 + random.nextInt(40)
val month = 1 + random.nextInt(12)
val day = random.nextInt(28)
adBuilder.setBirthday(new GregorianCalendar(year, month, day).getTime)
// Load Ad
adView.loadAd(adBuilder.build())
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
preferences.setProfile(profile)
app.refreshContainerHolder()
}
override def onStart() {
super.onStart()
registerCallback()
setListeningForBandwidth(true)
}
override def onBackPressed(): Unit =
if (drawer.isDrawerOpen) drawer.closeDrawer() else if (currentFragment != profilesFragment) {
displayFragment(profilesFragment)
drawer.setSelection(DRAWER_PROFILES)
} else super.onBackPressed()
override def onStop() {
setListeningForBandwidth(false)
super.onStop()
unregisterCallback()
clearDialog()
}
override def onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
drawer.saveInstanceState(outState)
if (crossfader != null) crossfader.saveInstanceState(outState)
}
override def onDestroy() {
super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
}
def recovery() {
if (serviceStarted) serviceStop()
val h = showProgress(R.string.recovering)
Utils.ThrowableFuture {
app.copyAssets()
h.sendEmptyMessage(0)
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK =>
serviceLoad()
case _ =>
cancelStart()
Log.e(TAG, "Failed to start VpnService")
}
def serviceStop() {
if (bgService != null) bgService.use(-1)
}
/** Called when connect button is clicked. */
def serviceLoad() {
bgService.use(app.profileId)
if (app.isVpnEnabled) {
changeSwitch(checked = false)
}
}
def clearDialog() {
if (progressDialog != null && progressDialog.isShowing) {
if (!isDestroyed) progressDialog.dismiss()
progressDialog = null
}
}
}
......@@ -20,77 +20,33 @@
package com.github.shadowsocks
import android.content.pm.ShortcutManager
import android.content.res.Resources
import android.os.{Build, Bundle}
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.CheckedTextView
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.Utils
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
/**
* Created by Lucas on 3/10/16.
*/
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
class ProfileConfigActivity extends Activity {
private lazy val child = getFragmentManager.findFragmentById(R.id.content).asInstanceOf[ProfileConfigFragment]
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_quick_switch)
setContentView(R.layout.layout_profile_config)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.quick_switch)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val lm = new LinearLayoutManager(this)
profilesList.setLayoutManager(lm)
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")
toolbar.setTitle(R.string.profile_config)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => onBackPressed())
toolbar.inflateMenu(R.menu.profile_config_menu)
toolbar.setOnMenuItemClickListener(child)
}
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()
}
}
......@@ -20,134 +20,151 @@
package com.github.shadowsocks
import java.nio.charset.Charset
import java.util.GregorianCalendar
import android.app.TaskStackBuilder
import android.content._
import android.content.pm.PackageManager
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.os.{Build, Bundle, Handler, UserManager}
import android.provider.Settings
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.os.Bundle
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget._
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.text.style.TextAppearanceSpan
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
import android.view.View.OnLongClickListener
import android.view._
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
import com.github.clans.fab.{FloatingActionButton, FloatingActionMenu}
import android.widget.{LinearLayout, PopupMenu, TextView, Toast}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.utils._
import com.github.shadowsocks.widget.UndoSnackbarManager
import net.glxn.qrgen.android.QRCode
import com.google.android.gms.ads.{AdRequest, AdSize, NativeExpressAdView}
import scala.collection.mutable.ArrayBuffer
import scala.util.Random
final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
with View.OnClickListener with CreateNdefMessageCallback {
object ProfilesFragment {
var instance: ProfilesFragment = _ // used for callback from ProfileManager and stateChanged from MainActivity
}
final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
import ProfilesFragment._
private val cardButtonLongClickListener: OnLongClickListener = view => {
Utils.positionToast(Toast.makeText(getActivity, view.getContentDescription, Toast.LENGTH_SHORT), view,
getActivity.getWindow, 0, Utils.dpToPx(getActivity, 8)).show()
true
}
/**
* Is ProfilesFragment editable at all.
*/
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
case State.CONNECTED | State.STOPPED => true
case _ => false
}
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
case State.CONNECTED => id != app.profileId
case State.STOPPED => true
case _ => false
}
private final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnKeyListener {
final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
private val title = itemView.findViewById(R.id.title).asInstanceOf[TextView]
private val address = itemView.findViewById(R.id.address).asInstanceOf[TextView]
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
private val indicator = itemView.findViewById(R.id.indicator).asInstanceOf[ViewGroup]
private val edit = itemView.findViewById(R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this)
itemView.setOnKeyListener(this)
private var adView: NativeExpressAdView = _
{
val shareBtn = itemView.findViewById(R.id.share)
shareBtn.setOnClickListener(_ => {
val url = item.toString
if (isNfcBeamEnabled) {
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this)
nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
}
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.close, null)
.setNegativeButton(R.string.copy_url, ((_, _) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
.setView(image)
.setTitle(R.string.share)
.create()
if (!isNfcAvailable) dialog.setMessage(getString(R.string.share_message_without_nfc))
else if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc),
((_, _) => startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.share_message))
dialog.setOnDismissListener(_ =>
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
}
dialog.show()
})
shareBtn.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.share, Toast.LENGTH_SHORT), shareBtn,
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show()
true
val share = itemView.findViewById(R.id.share)
share.setOnClickListener(_ => {
val popup = new PopupMenu(getActivity, share)
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
popup.setOnMenuItemClickListener(this)
popup.show()
})
}
def updateText(txTotal: Long = 0, rxTotal: Long = 0) {
val builder = new SpannableStringBuilder
val tx = item.tx + txTotal
val rx = item.rx + rxTotal
builder.append(if (isDemoMode) "Profile #" + item.id else item.name)
if (tx != 0 || rx != 0) {
val start = builder.length
builder.append(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
builder.setSpan(new TextAppearanceSpan(ProfileManagerActivity.this, android.R.style.TextAppearance_Small),
start + 1, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
handler.post(() => text.setText(builder))
share.setOnLongClickListener(cardButtonLongClickListener)
}
def bind(item: Profile) {
this.item = item
updateText()
val editable = isProfileEditable(item.id)
edit.setEnabled(editable)
edit.setAlpha(if (editable) 1 else 0.5F)
var tx = item.tx
var rx = item.rx
if (item.id == app.profileId) {
tx += txTotal
rx += rxTotal
}
title.setText(item.getName)
address.setText(if (item.nameIsEmpty) "" else item.formattedAddress)
traffic.setText(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
if (item.id == app.profileId) {
text.setChecked(true)
indicator.setBackgroundResource(R.drawable.background_selected)
selectedItem = this
} else {
text.setChecked(false)
indicator.setBackgroundResource(R.drawable.background_selectable)
if (selectedItem eq this) selectedItem = null
}
if (item.host == "198.199.101.152") {
if (adView == null) {
val params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_HORIZONTAL
params.setMargins(0, Utils.dpToPx(getActivity, 8), 0, 0)
adView = new NativeExpressAdView(getActivity)
adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
adView.setAdSize(new AdSize(328, 132))
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
// Demographics
val random = new Random()
val adBuilder = new AdRequest.Builder()
adBuilder.setGender(AdRequest.GENDER_MALE)
val year = 1975 + random.nextInt(40)
val month = 1 + random.nextInt(12)
val day = random.nextInt(28)
adBuilder.setBirthday(new GregorianCalendar(year, month, day).getTime)
// Load Ad
adView.loadAd(adBuilder.build())
} else adView.setVisibility(View.VISIBLE)
} else if (adView != null) adView.setVisibility(View.GONE)
}
def onClick(v: View) {
def onClick(v: View): Unit = if (isEnabled) {
val activity = getActivity.asInstanceOf[MainActivity]
val old = app.profileId
app.switchProfile(item.id)
finish()
profilesAdapter.refreshId(old)
bind(item)
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
}
def onKey(v: View, keyCode: Int, event: KeyEvent): Boolean =
if (event.getAction == KeyEvent.ACTION_DOWN) keyCode match {
case KeyEvent.KEYCODE_DPAD_LEFT =>
val index = getAdapterPosition
if (index >= 0) {
profilesAdapter.remove(index)
undoManager.remove(index, item)
true
} else false
case _ => false
} else false
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
case R.id.action_qr_code_nfc =>
getFragmentManager.beginTransaction().add(new QRCodeDialog(item.toString), "").commitAllowingStateLoss()
true
case R.id.action_export =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, item.toString))
true
case _ => false
}
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
final class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
var profiles = new ArrayBuffer[Profile]
profiles ++= app.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
......@@ -158,6 +175,9 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
setHasStableIds(true) // Reason: http://stackoverflow.com/a/32488059/2245107
override def getItemId(position: Int): Long = profiles(position).id
def add(item: Profile) {
undoManager.flush()
val pos = getItemCount
......@@ -192,59 +212,68 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
profiles.insert(index, item)
notifyItemInserted(index)
}
def commit(actions: Iterator[(Int, Profile)]): Unit = for ((_, item) <- actions) {
app.profileManager.delProfile(item.id)
if (item.id == app.profileId) app.profileId(-1)
def commit(actions: Iterator[(Int, Profile)]): Unit =
for ((_, item) <- actions) app.profileManager.delProfile(item.id)
def refreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) {
profiles(index) = app.profileManager.getProfile(id).get
notifyItemChanged(index)
}
}
def removeId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) {
profiles.remove(index)
notifyItemRemoved(index)
if (id == app.profileId) app.profileId(0) // switch to null profile
}
}
}
private var selectedItem: ProfileViewHolder = _
private val handler = new Handler
private var menu : FloatingActionMenu = _
private lazy val profilesAdapter = new ProfilesAdapter
lazy val profilesAdapter = new ProfilesAdapter
private var undoManager: UndoSnackbarManager[Profile] = _
private var txTotal: Long = _
private var rxTotal: Long = _
private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private lazy val isDemoMode = Build.VERSION.SDK_INT >= 25 && getSystemService(classOf[UserManager]).isDemoUser
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private var nfcAdapter : NfcAdapter = _
private var nfcShareItem: Array[Byte] = _
private var isNfcAvailable: Boolean = _
private var isNfcEnabled: Boolean = _
private var isNfcBeamEnabled: Boolean = _
private def startConfig(id: Int) = startActivity(new Intent(getActivity, classOf[ProfileConfigActivity])
.putExtra(Action.EXTRA_PROFILE_ID, id))
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profiles)
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_profiles, container, false)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.profiles)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
})
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
initFab()
app.profileManager.setProfileAddedListener(profilesAdapter.add)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(getActivity)
profilesList.setLayoutManager(layoutManager)
profilesList.setItemAnimator(new DefaultItemAnimator)
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
profilesList.setItemAnimator(animator)
profilesList.setAdapter(profilesAdapter)
instance = this
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
}.getOrElse(-1))
undoManager = new UndoSnackbarManager[Profile](profilesList, profilesAdapter.undo, profilesAdapter.commit)
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar), profilesAdapter.undo, profilesAdapter.commit)
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isProfileEditable(viewHolder.asInstanceOf[ProfileViewHolder].item.id))
super.getSwipeDirs(recyclerView, viewHolder) else 0
override def getDragDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
def onSwiped(viewHolder: ViewHolder, direction: Int) {
val index = viewHolder.getAdapterPosition
profilesAdapter.remove(index)
......@@ -255,169 +284,48 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
true
}
}).attachToRecyclerView(profilesList)
attachService(new IShadowsocksServiceCallback.Stub {
def stateChanged(state: Int, profileName: String, msg: String): Unit = () // ignore
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
})
if (app.settings.getBoolean(Key.profileTip, true)) {
app.editor.putBoolean(Key.profileTip, false).apply()
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create().show()
}
val intent = getIntent
if (intent != null) handleShareIntent(intent)
}
def initFab() {
menu = findViewById(R.id.menu).asInstanceOf[FloatingActionMenu]
menu.setClosedOnTouchOutside(true)
val dm = AppCompatDrawableManager.get
val manualAddFAB = findViewById(R.id.fab_manual_add).asInstanceOf[FloatingActionButton]
manualAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_create))
manualAddFAB.setOnClickListener(this)
val qrcodeAddFAB = findViewById(R.id.fab_qrcode_add).asInstanceOf[FloatingActionButton]
qrcodeAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_image_camera_alt))
qrcodeAddFAB.setOnClickListener(this)
val nfcAddFAB = findViewById(R.id.fab_nfc_add).asInstanceOf[FloatingActionButton]
nfcAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_device_nfc))
nfcAddFAB.setOnClickListener(this)
val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste))
importAddFAB.setOnClickListener(this)
menu.setOnMenuToggleListener(opened => if (opened) qrcodeAddFAB.setVisibility(
if (getPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) View.VISIBLE else View.GONE))
override def onTrafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = {
this.txTotal = txTotal
this.rxTotal = rxTotal
profilesAdapter.refreshId(app.profileId)
}
override def onResume() {
super.onResume()
updateNfcState()
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
override def onClick(v: View){
v.getId match {
case R.id.fab_manual_add =>
menu.toggle(true)
val profile = app.profileManager.createProfile()
app.profileManager.updateProfile(profile)
app.switchProfile(profile.id)
finish()
case R.id.fab_qrcode_add =>
menu.toggle(false)
val intent = new Intent(this, classOf[ScannerActivity])
startActivity(intent)
case R.id.fab_nfc_add =>
menu.toggle(true)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.gotcha, null)
.setTitle(R.string.add_profile_nfc_hint_title)
.create()
if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.add_profile_nfc_hint))
}
dialog.show()
case R.id.fab_import_add =>
menu.toggle(true)
if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return
}
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
def updateNfcState() {
isNfcAvailable = false
isNfcEnabled = false
isNfcBeamEnabled = false
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter != null) {
isNfcAvailable = true
if (nfcAdapter.isEnabled) {
isNfcEnabled = true
if (nfcAdapter.isNdefPushEnabled) {
isNfcBeamEnabled = true
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
}
}
}
}
def handleShareIntent(intent: Intent) {
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
finish()
return
}
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
.setMessage(profiles.mkString("\n"))
.create()
dialog.show()
}
override def onStart() {
super.onStart()
registerCallback
}
override def onStop() {
super.onStop()
unregisterCallback
override def onDetach() {
undoManager.flush()
super.onDetach()
}
override def onDestroy() {
detachService()
undoManager.flush()
app.profileManager.setProfileAddedListener(null)
instance = null
super.onDestroy()
}
override def onBackPressed() {
if (menu.isOpened) menu.close(true) else super.onBackPressed()
}
def createNdefMessage(nfcEvent: NfcEvent) =
new NdefMessage(Array(new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem)))
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code =>
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile)
Toast.makeText(getActivity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return true
}
}
Toast.makeText(getActivity, R.string.action_import_err, Toast.LENGTH_SHORT).show()
true
case R.id.action_manual_settings =>
startConfig(app.profileManager.createProfile().id)
true
case R.id.action_export =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
Toast.makeText(getActivity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(getActivity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
}
true
case _ => false
......
/*******************************************************************************/
/* */
/* 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
}
}
def navigateUp() {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
}
override def onCreate(state: Bundle) {
super.onCreate(state)
setContentView(R.layout.layout_scanner)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getTitle)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
toolbar.setNavigationOnClickListener(_ => navigateUp())
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => finish())
scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
}
......@@ -98,7 +91,9 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand
val uri = rawResult.getText
if (!TextUtils.isEmpty(uri))
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
* @author Mygod
*/
trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
class ShadowsocksServiceConnection extends ServiceConnection {
private class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
binder = service
service.linkToDeath(ServiceBoundContext.this, 0)
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()
}
override def onServiceDisconnected(name: ComponentName) {
......@@ -46,33 +52,37 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
}
}
protected def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try {
bgService.registerCallback(callback)
callbackRegistered = true
} catch {
case _: RemoteException => // Nothing
protected def setListeningForBandwidth(value: Boolean) {
if (listeningForBandwidth != value && bgService != null && callback != null)
if (value) bgService.startListeningForBandwidth(callback) else bgService.stopListeningForBandwidth(callback)
listeningForBandwidth = value
}
protected def unregisterCallback() {
private def unregisterCallback() {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case _: RemoteException =>
}
listeningForBandwidth = false
callbackRegistered = false
}
def onServiceConnected(): Unit = ()
def onServiceDisconnected(): Unit = ()
protected def onServiceConnected(): Unit = ()
/**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
protected def onServiceDisconnected(): Unit = ()
override def binderDied(): Unit = ()
private var callback: IShadowsocksServiceCallback.Stub = _
private var connection: ShadowsocksServiceConnection = _
private var callbackRegistered: Boolean = _
private var listeningForBandwidth: Boolean = _
// Variables
var binder: IBinder = _
private var binder: IBinder = _
var bgService: IShadowsocksService = _
def attachService(callback: IShadowsocksServiceCallback.Stub = null) {
protected def attachService(callback: IShadowsocksServiceCallback.Stub = null) {
this.callback = callback
if (bgService == null) {
val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
......@@ -85,8 +95,9 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
}
}
def detachService() {
protected def detachService() {
unregisterCallback()
onServiceDisconnected()
callback = null
if (connection != null) {
try unbindService(connection) catch {
......
......@@ -83,13 +83,14 @@ class ShadowsocksApplication extends Application {
.setFatal(false)
.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 currentProfile: Option[Profile] = profileManager.getProfile(profileId)
def switchProfile(id: Int): Profile = {
profileId(id)
profileManager.getProfile(id) getOrElse profileManager.createProfile()
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
profileId(result.id)
result
}
private def checkChineseLocale(locale: Locale): Locale = if (locale.getLanguage == "zh") locale.getCountry match {
......
......@@ -315,7 +315,7 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route)
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) {
......
......@@ -24,13 +24,12 @@ import java.util.Locale
import android.app.{KeyguardManager, NotificationManager, PendingIntent}
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.BigTextStyle
import android.support.v4.content.ContextCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{TrafficMonitor, Action, State, Utils}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
/**
* @author Mygod
......@@ -57,17 +56,11 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.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))
.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))
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 var isVisible = true
update(if (service.getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager].isScreenOn)
......@@ -87,6 +80,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
case Intent.ACTION_SCREEN_ON =>
setVisible(visible && Utils.isLollipopOrAbove && !keyGuard.inKeyguardRestrictedInputMode, forceShow)
service.binder.registerCallback(callback)
service.binder.startListeningForBandwidth(callback)
callbackRegistered = true
case Intent.ACTION_USER_PRESENT => setVisible(visible = true, forceShow = forceShow)
}
......
/*******************************************************************************/
/* */
/* 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.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.{Intent, SharedPreferences}
import android.net.Uri
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.{DropDownPreference, Preference}
import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen, Utils}
import be.mygod.preference._
object ShadowsocksSettings {
// Constants
private final val TAG = "ShadowsocksSettings"
private val PROXY_PREFS = Array(Key.name, Key.host, Key.remotePort, Key.localPort, Key.password, Key.method,
Key.auth, Key.kcp, Key.kcpPort, Key.kcpcli)
private val FEATURE_PREFS = Array(Key.route, Key.remoteDns, Key.proxyApps, Key.udpdns, Key.ipv6)
// Helper functions
def updateDropDownPreference(pref: Preference, value: String) {
pref.asInstanceOf[DropDownPreference].setValue(value)
}
def updateEditTextPreference(pref: Preference, value: String) {
pref.asInstanceOf[EditTextPreference].setText(value)
}
def updateNumberPickerPreference(pref: Preference, value: Int) {
pref.asInstanceOf[NumberPickerPreference].setValue(value)
}
def updateSwitchPreference(pref: Preference, value: Boolean) {
pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile, demo: Boolean = false) {
name match {
case Key.name =>
updateEditTextPreference(pref, profile.name)
pref.setSummary(if (demo) "Profile #" + profile.id else "%s")
case Key.host =>
updateEditTextPreference(pref, profile.host)
pref.setSummary(if (demo) "shadowsocks.example.org" else "%s")
case Key.remotePort =>
updateNumberPickerPreference(pref, profile.remotePort)
pref.setSummary(if (demo) "1337" else "%d")
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
case Key.password =>
updateEditTextPreference(pref, profile.password)
pref.setSummary(if (demo) "\u2022" * 32 else "%s")
case Key.remoteDns =>
updateEditTextPreference(pref, profile.remoteDns)
if (profile.remoteDns != null)
pref.setSummary(if (demo) "8.8.8.8" else "%s")
case Key.method => updateDropDownPreference(pref, profile.method)
case Key.route => updateDropDownPreference(pref, profile.route)
case Key.proxyApps => updateSwitchPreference(pref, profile.proxyApps)
case Key.udpdns => updateSwitchPreference(pref, profile.udpdns)
case Key.auth => updateSwitchPreference(pref, profile.auth)
case Key.ipv6 => updateSwitchPreference(pref, profile.ipv6)
case Key.kcp => updateSwitchPreference(pref, profile.kcp)
case Key.kcpPort => updateNumberPickerPreference(pref, profile.kcpPort)
case Key.kcpcli => updateEditTextPreference(pref, profile.kcpcli)
}
}
}
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
import ShadowsocksSettings._
private def activity = getActivity.asInstanceOf[Shadowsocks]
lazy val natSwitch: SwitchPreference = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
private var isProxyApps: SwitchPreference = _
override def onCreatePreferences(bundle: Bundle, key: String) {
addPreferencesFromResource(R.xml.pref_all)
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
findPreference(Key.name).setOnPreferenceChangeListener((_, value) => {
profile.name = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.host).setOnPreferenceChangeListener((_, value) => {
profile.host = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.remotePort).setOnPreferenceChangeListener((_, value) => {
profile.remotePort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.localPort).setOnPreferenceChangeListener((_, value) => {
profile.localPort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.password).setOnPreferenceChangeListener((_, value) => {
profile.password = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.method).setOnPreferenceChangeListener((_, value) => {
profile.method = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.route).setOnPreferenceChangeListener((_, value) => {
profile.route = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.remoteDns).setOnPreferenceChangeListener((_, value) => {
profile.remoteDns = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcp).setOnPreferenceChangeListener((_, value) => {
profile.kcp = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcpPort).setOnPreferenceChangeListener((_, value) => {
profile.kcpPort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcpcli).setOnPreferenceChangeListener((_, value) => {
profile.kcpcli = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(activity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
isProxyApps.setOnPreferenceChangeListener((_, value) => {
profile.proxyApps = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.udpdns).setOnPreferenceChangeListener((_, value) => {
profile.udpdns = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.auth).setOnPreferenceChangeListener((_, value) => {
profile.auth = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.ipv6).setOnPreferenceChangeListener((_, value) => {
profile.ipv6 = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(activity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(activity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(activity))
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(activity.findViewById(android.R.id.content), 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")))
}
findPreference("recovery").setOnPreferenceClickListener(_ => {
app.track(TAG, "reset")
activity.recovery()
true
})
findPreference("about").setOnPreferenceClickListener(_ => {
app.track(TAG, "about")
val web = new WebView(activity)
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
}
})
new AlertDialog.Builder(activity)
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
.setNegativeButton(getString(android.R.string.ok), null)
.setView(web)
.create()
.show()
true
})
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = preference.getKey match {
case Key.kcpcli => displayPreferenceDialog(Key.kcpcli, new KcpCliPreferenceDialogFragment())
case _ => super.onDisplayPreferenceDialog(preference)
}
def refreshProfile() {
profile = app.currentProfile match {
case Some(p) => p
case None =>
app.profileManager.getFirstProfile match {
case Some(p) =>
app.profileId(p.id)
p
case None =>
val default = app.profileManager.createDefault()
app.profileId(default.id)
default
}
}
isProxyApps.setChecked(profile.proxyApps)
}
override def onDestroy() {
super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this)
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT =>
activity.handler.post(() => {
activity.detachService()
activity.attachServiceCallback()
})
case _ =>
}
private var enabled = true
def setEnabled(enabled: Boolean) {
this.enabled = enabled
for (name <- Key.isNAT #:: PROXY_PREFS.toStream #::: FEATURE_PREFS.toStream) {
val pref = findPreference(name)
if (pref != null) pref.setEnabled(enabled &&
(name != Key.proxyApps || Utils.isLollipopOrAbove || app.isNatEnabled))
}
}
var profile: Profile = _
def setProfile(profile: Profile) {
this.profile = profile
val demo = Build.VERSION.SDK_INT >= 25 && activity.getSystemService(classOf[UserManager]).isDemoUser
for (name <- Array(PROXY_PREFS, FEATURE_PREFS).flatten) updatePreference(findPreference(name), name, profile, demo)
}
}
......@@ -148,7 +148,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Route.ALL)
AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.name)
notification = new ShadowsocksNotification(this, profile.getName)
}
/** Called when the activity is first created. */
......@@ -318,7 +318,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val builder = new Builder()
builder
.setSession(profile.name)
.setSession(profile.getName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
......
......@@ -52,7 +52,7 @@ class TaskerActivity extends AppCompatActivity {
}
def bind(item: Profile) {
this.item = item
text.setText(item.name)
text.setText(item.getName)
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
import java.net.URLEncoder
import java.util.Locale
import android.content.SharedPreferences
import android.os.Binder
import android.util.Base64
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile {
......@@ -32,10 +34,10 @@ class Profile {
var id: Int = 0
@DatabaseField
var name: String = "Untitled"
var name: String = ""
@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
@DatabaseField
......@@ -45,7 +47,7 @@ class Profile {
var remotePort: Int = 8388
@DatabaseField
var password: String = ""
var password: String = "u1rRWTssNv0p"
@DatabaseField
var method: String = "aes-256-cfb"
......@@ -95,9 +97,53 @@ class Profile {
@DatabaseField
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,
method, if (auth) "-auth" else "", password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP) +
'#' + URLEncoder.encode(name, "utf-8")
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 @@
package com.github.shadowsocks.database
import android.util.Log
import com.github.shadowsocks.ProfilesFragment
import com.github.shadowsocks.ShadowsocksApplication.app
object ProfileManager {
......@@ -30,9 +31,6 @@ object ProfileManager {
class ProfileManager(dbHelper: DBHelper) {
import ProfileManager._
var profileAddedListener: Profile => Any = _
def setProfileAddedListener(listener: Profile => Any): Unit = this.profileAddedListener = listener
def createProfile(p: Profile = null): Profile = {
val profile = if (p == null) new Profile else p
profile.id = 0
......@@ -52,7 +50,7 @@ class ProfileManager(dbHelper: DBHelper) {
.prepareStatementString).getFirstResult
if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1
dbHelper.profileDao.createOrUpdate(profile)
if (profileAddedListener != null) profileAddedListener(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.add(profile)
} catch {
case ex: Exception =>
Log.e(TAG, "addProfile", ex)
......@@ -90,6 +88,7 @@ class ProfileManager(dbHelper: DBHelper) {
def delProfile(id: Int): Boolean = {
try {
dbHelper.profileDao.deleteById(id)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.removeId(id)
true
} catch {
case ex: Exception =>
......@@ -122,14 +121,4 @@ class ProfileManager(dbHelper: DBHelper) {
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 {
val localPort = "localPortNum"
val remoteDns = "remoteDns"
val profileTip = "profileTip"
val kcp = "kcp"
val kcpPort = "kcpPort"
val kcpcli = "kcpcli"
val dirty = "profileDirty"
val tfo = "tcp_fastopen"
val currentVersionCode = "currentVersionCode"
}
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 CONNECTED = 2
val STOPPING = 3
val STOPPED = 4
def isAvailable(state: Int): Boolean = state != CONNECTED && state != CONNECTING
}
object Action {
val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE"
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
object Route {
......
......@@ -38,8 +38,7 @@ object Parser {
profile.method = ss.group(2).toLowerCase
if (ss.group(3) != null) profile.auth = true
profile.password = ss.group(4)
profile.name = ss.group(5)
profile.host = profile.name
profile.host = ss.group(5)
profile.remotePort = ss.group(6).toInt
if (m.group(2) != null) profile.name = URLDecoder.decode(m.group(3), "utf-8")
profile
......
......@@ -46,7 +46,7 @@ class TaskerSettings(bundle: Bundle) {
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
new Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
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)
})
}
......
/*******************************************************************************/
/* */
/* 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 @@
package com.github.shadowsocks.widget
import android.animation.ValueAnimator
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.view.View
import com.github.clans.fab.FloatingActionMenu
import scala.collection.JavaConverters._
import android.view.View.MeasureSpec
import com.github.shadowsocks.R
/**
* Behavior for com.github.clans.fab.FloatingActionMenu that is aware of Snackbars and scrolling.
* Based on: http://stackoverflow.com/a/6212120/2245107
*
* @author Mygod
*/
class FloatingActionMenuBehavior(context: Context, attrs: AttributeSet)
extends Behavior[FloatingActionMenu](context, attrs) {
private var fabTranslationYAnimator: ValueAnimator = _
private var fabTranslationY: Float = _
override def layoutDependsOn(parent: CoordinatorLayout, child: FloatingActionMenu, dependency: View): Boolean =
dependency.isInstanceOf[SnackbarLayout]
trait BoundedView extends View {
private var boundedWidth: Int = _
private var boundedHeight: Int = _
override def onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionMenu,
dependency: View): Boolean = {
dependency match {
case _: SnackbarLayout =>
var targetTransY = parent.getDependencies(child).asScala
.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
protected def initAttrs(context: Context, attrs: AttributeSet) {
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedView)
boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0)
boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0)
arr.recycle()
}
override def onStartNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, directTargetChild: View,
target: View, nestedScrollAxes: Int) = true
override def onNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, target: View, dxConsumed: Int,
dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
super.onNestedScroll(parent, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
val dy = dyConsumed + dyUnconsumed
if (child.isMenuButtonHidden) {
if (dy < 0) child.showMenuButton(true)
} else if (dy > 0) child.hideMenuButton(true)
}
override def onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int): Unit = super.onMeasure(
if (boundedWidth > 0 && boundedWidth < MeasureSpec.getSize(widthMeasureSpec))
MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthMeasureSpec))
else widthMeasureSpec,
if (boundedHeight > 0 && boundedHeight < MeasureSpec.getSize(heightMeasureSpec))
MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightMeasureSpec))
else heightMeasureSpec)
}
......@@ -22,6 +22,7 @@ package com.github.shadowsocks.widget
import android.support.design.widget.Snackbar
import android.view.View
import com.github.shadowsocks.R
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