Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d295b28bb | ||
|
|
985b3a6b7d | ||
|
|
78c17a6660 | ||
|
|
202bd2c984 | ||
|
|
3c14ff67ce | ||
|
|
e05abd7c24 | ||
|
|
c8bb8fc929 | ||
|
|
faac54356e | ||
|
|
44a63ef1c4 | ||
|
|
7d9686b5b5 | ||
|
|
8453267996 | ||
|
|
29c5011ce4 | ||
|
|
f1b1b64f6b | ||
|
|
da8e41d7bb | ||
|
|
062dfb35c0 | ||
|
|
cb44ebaf03 | ||
|
|
e2a3184715 | ||
|
|
88131c3441 | ||
|
|
d437f035cc | ||
|
|
9cb8bf681a | ||
|
|
fb1a7e17cc | ||
|
|
b6e15f2f13 | ||
|
|
2834621b7e | ||
|
|
8a86176ded |
@@ -10,9 +10,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
||||
|
||||
### PREREQUISITES
|
||||
|
||||
* JDK 1.7
|
||||
* JDK 1.8
|
||||
* SBT 0.13.0+
|
||||
* Android SDK r24+
|
||||
* Android SDK r25+
|
||||
* Android NDK r10d+
|
||||
|
||||
### BUILD
|
||||
|
||||
@@ -36,13 +36,14 @@ libraryDependencies ++= Seq(
|
||||
"com.google.android.gms" % "play-services-base" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-analytics" % "8.4.0",
|
||||
"com.android.support" % "design" % "23.2.1",
|
||||
"com.android.support" % "gridlayout-v7" % "23.2.1",
|
||||
"com.android.support" % "cardview-v7" % "23.2.1",
|
||||
"com.android.support" % "design" % "23.3.0",
|
||||
"com.android.support" % "gridlayout-v7" % "23.3.0",
|
||||
"com.android.support" % "cardview-v7" % "23.3.0",
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
|
||||
"com.j256.ormlite" % "ormlite-core" % "4.48",
|
||||
"com.j256.ormlite" % "ormlite-android" % "4.48",
|
||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2"
|
||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2",
|
||||
"com.github.clans" % "fab" % "1.6.3"
|
||||
)
|
||||
|
||||
proguardOptions in Android ++= Seq("-keep class com.github.shadowsocks.** { <init>(...); }",
|
||||
|
||||
+2
-10
@@ -1,11 +1,3 @@
|
||||
resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-snapshots"))(Resolver.ivyStylePatterns)
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.0")
|
||||
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.19")
|
||||
|
||||
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.7.0-SNAPSHOT")
|
||||
|
||||
resolvers += Resolver.url("rtimush/sbt-plugin-snapshots", new URL("https://dl.bintray.com/rtimush/sbt-plugin-snapshots/"))(Resolver.ivyStylePatterns)
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.9-6-g5a7705c")
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.shadowsocks"
|
||||
android:versionCode="134"
|
||||
android:versionName="2.10.1">
|
||||
xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks"
|
||||
android:versionCode="136"
|
||||
android:versionName="2.10.3">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
@@ -11,6 +11,8 @@
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
||||
|
||||
@@ -25,7 +27,8 @@
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:backupAgent=".ShadowsocksBackupAgent"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Material">
|
||||
android:theme="@style/Theme.Material"
|
||||
android:banner="@drawable/ic_start_connected">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
@@ -43,25 +46,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ParserActivity"
|
||||
android:theme="@style/PopupTheme"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="ss"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="ss" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -75,12 +60,23 @@
|
||||
android:name=".ProfileManagerActivity"
|
||||
android:label="@string/profiles"
|
||||
android:parentActivityName=".Shadowsocks"
|
||||
android:exported="false"
|
||||
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"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="ss"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="ss" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -100,7 +96,7 @@
|
||||
android:launchMode="singleInstance"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/PopupTheme.QuickSwitch">
|
||||
android:theme="@style/PopupTheme.QuickSwitch">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.QUICK_SWITCH" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
@@ -136,7 +132,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".TaskerReceiver">
|
||||
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
|
||||
+1
-1
Submodule src/main/jni/openssl updated: 7388b9ff18...c51144d76d
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19.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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="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"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<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="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"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"/>
|
||||
</LinearLayout>
|
||||
@@ -45,10 +45,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
<ListView
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:id="@+id/applistview"
|
||||
android:visibility="gone"/>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemicon"
|
||||
@@ -15,20 +16,19 @@
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemtext"
|
||||
<Switch
|
||||
android:id="@+id/itemcheck"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="fill_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:ellipsize="end"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/itemcheck"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:textSize="18sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:clickable="false"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,14 +3,61 @@
|
||||
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">
|
||||
<include layout="@layout/toolbar_light_dark" />
|
||||
<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="88dip"
|
||||
android:paddingLeft="16dip" android:paddingRight="88dip"
|
||||
android:paddingTop="16dip" android:paddingBottom="16dip"
|
||||
android:background="@color/material_accent_300" android:orientation="vertical">
|
||||
<include layout="@layout/layout_main_stat_title"/>
|
||||
<android.support.v7.widget.GridLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" 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_marginRight="24dp" 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_marginRight="24dp" 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>
|
||||
</LinearLayout>
|
||||
<fragment android:name="com.github.shadowsocks.ShadowsocksSettings"
|
||||
android:id="@android:id/content" android:layout_below="@+id/toolbar"
|
||||
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"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:elevation="6dp"
|
||||
android:layout_alignBottom="@+id/toolbar"
|
||||
android:layout_alignBottom="@+id/title_bar"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginRight="16dp"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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="wrap_content">
|
||||
<TextView android:text="@string/stat" android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorPrimary" android:ellipsize="marquee"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"/>
|
||||
<TextView android:id="@+id/connection_test"
|
||||
android:textSize="14sp" android:textColor="?android:attr/textColorSecondary"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
@@ -1,16 +1,73 @@
|
||||
<?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:orientation="vertical"
|
||||
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:layout_height="0dp" android:layout_weight="1">
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
|
||||
app:layout_behavior="com.github.shadowsocks.widget.AutoPaddingBehavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="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_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:menu_colorNormal="@color/material_accent_500"
|
||||
app:menu_colorPressed="@color/material_accent_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"
|
||||
android:src="@drawable/ic_content_create"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_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"
|
||||
android:src="@drawable/ic_image_camera_alt"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_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"
|
||||
android:src="@drawable/ic_device_nfc"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_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"
|
||||
android:src="@drawable/ic_content_paste"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:fab_size="mini"
|
||||
app:fab_label="@string/action_import"
|
||||
/>
|
||||
</com.github.clans.fab.FloatingActionMenu>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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:paddingLeft="20dp"
|
||||
android:paddingRight="?attr/dialogPreferredPadding">
|
||||
android:paddingRight="?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"
|
||||
@@ -30,5 +33,7 @@
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_social_share"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/share"/>
|
||||
android:contentDescription="@string/share"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@+id/container"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="80dp" android:maxWidth="80dp" android:gravity="center"
|
||||
android:textSize="70sp" android:textColor="#ffffffff"
|
||||
android:background="#99000088" android:padding="10dp"
|
||||
android:visibility="invisible"/>
|
||||
@@ -1,124 +0,0 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/widget_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="15dip"
|
||||
android:paddingRight="15dip"
|
||||
android:paddingTop="6dip"
|
||||
android:paddingBottom="6dip"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end" />
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.v7.widget.GridLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:columnCount="4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
app:layout_column="0"
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/sent" />
|
||||
<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:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="▲" />
|
||||
<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:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="end"
|
||||
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>
|
||||
@@ -1,12 +1,15 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:showAsAction="never"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="1"/>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:showAsAction="never"
|
||||
android:icon="@drawable/ic_content_copy"
|
||||
app:showAsAction="ifRoom"
|
||||
android:alphabeticShortcut="e"
|
||||
android:numericShortcut="1"/>
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:icon="@drawable/ic_content_paste"
|
||||
app:showAsAction="ifRoom"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="2"/>
|
||||
</menu>
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
<?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_content_add"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="0"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item android:id="@+id/scan_qr_code"
|
||||
android:title="@string/add_profile_methods_scan_qr_code"
|
||||
android:alphabeticShortcut="s"
|
||||
android:numericShortcut="1"/>
|
||||
<item android:id="@+id/manual_settings"
|
||||
android:title="@string/add_profile_methods_manual_settings"
|
||||
android:alphabeticShortcut="m"
|
||||
android:numericShortcut="2"/>
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="3"/>
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:alphabeticShortcut="e"
|
||||
android:numericShortcut="4"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:id="@+id/action_export"
|
||||
android:title="@string/action_export"
|
||||
android:alphabeticShortcut="e"
|
||||
android:numericShortcut="1"
|
||||
android:icon="@drawable/ic_content_copy"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
|
||||
Executable → Regular
+7
-7
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--Generated by crowdin.net-->
|
||||
<resources>
|
||||
<string name="app_name">影梭</string>
|
||||
<string name="global_cat">全局设置</string>
|
||||
<string name="misc_cat">其他</string>
|
||||
<string name="proxy_cat">服务器设置</string>
|
||||
<string name="profile_name">配置名称</string>
|
||||
@@ -37,8 +35,8 @@
|
||||
<string name="nat_deprecated">警告:在 Android 5.0 及更高版本中不建议使用 NAT 模式</string>
|
||||
<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="stop">停止</string>
|
||||
<string name="quick_switch">切换</string>
|
||||
<string name="stopping">正在关闭……</string>
|
||||
<string name="vpn_error">后台服务启动失败:%s</string>
|
||||
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
|
||||
@@ -61,9 +59,9 @@
|
||||
<string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string>
|
||||
<string name="flushing">刷新中……</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="share_message">扫描二维码或者使用 NFC(Android Beam)</string>
|
||||
<string name="share_message">扫描二维码或使用 Android Beam (NFC)</string>
|
||||
<string name="share_message_without_nfc">扫描二维码</string>
|
||||
<string name="share_message_nfc_disabled">扫描二维码,NFC(Android Beam)被禁用</string>
|
||||
<string name="share_message_nfc_disabled">扫描二维码\nAndroid Beam 被禁用</string>
|
||||
<string name="turn_on_nfc">打开 NFC & Android Beam</string>
|
||||
<string name="add_profile">添加配置文件</string>
|
||||
|
||||
@@ -91,13 +89,15 @@
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_manager_dialog">配置文件</string>
|
||||
<string name="profile_manager_dialog_content">• 点击选择配置文件;\n• 滑动删除;\n•
|
||||
<string name="profile_manager_dialog_content">• 点击选择配置文件;\n• 向左/右滑动或按左删除;\n•
|
||||
长按拖动改变顺序。</string>
|
||||
<string name="gotcha">好的</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-array name="route_entry">
|
||||
<item>全局</item>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="proxied_apps_summary">為已選擇的應用程式設定 Proxy</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Shadowsocks</string>
|
||||
<string name="misc_cat">其他</string>
|
||||
<string name="proxy_cat">伺服器設定</string>
|
||||
<string name="profile_name">設定檔名稱</string>
|
||||
<string name="profile">設定檔</string>
|
||||
<string name="profile_summary">切換至其他設定檔或新增新設定檔</string>
|
||||
<string name="proxy">伺服器</string>
|
||||
<string name="proxy_summary">遠端伺服器的 IP 位址或主機名稱</string>
|
||||
<string name="remote_port">遠端連接埠</string>
|
||||
<string name="remote_port_summary">%d(遠端伺服器的連接埠號碼)</string>
|
||||
<string name="port">連接埠號碼</string>
|
||||
<string name="port_summary">%d(本機伺服器的連接埠號碼)</string>
|
||||
<string name="sitekey">密碼</string>
|
||||
<string name="sitekey_summary">遠端伺服器的密碼</string>
|
||||
<string name="enc_method">加密方法</string>
|
||||
<string name="feature_cat">功能設定</string>
|
||||
<string name="http_proxy">HTTP Proxy</string>
|
||||
<string name="http_proxy_summary">本機 HTTP Proxy</string>
|
||||
<string name="dns_proxy">DNS Proxy</string>
|
||||
<string name="dns_proxy_summary">重新導向 DNS 查詢至遠端</string>
|
||||
<string name="auto_set_gfwlist">中國大陸路由</string>
|
||||
<string name="auto_set_gfwlist_summary">略過一切中國大陸的網站</string>
|
||||
<string name="route_list">路由</string>
|
||||
<string name="proxied_apps">個別應用程式的 Proxy</string>
|
||||
<string name="proxied_apps_summary">為已選擇的應用程式設定 Proxy,在 Android 4.X 以下需要開啟 NAT 模式</string>
|
||||
<string name="on">開</string>
|
||||
<string name="bypass_apps">略過模式</string>
|
||||
<string name="bypass_apps_summary">啟用此選項,會略過已選擇的應用程式</string>
|
||||
<string name="auto_connect">自動連線</string>
|
||||
<string name="auto_connect_summary">在裝置啟動時啟用 Shadowsocks</string>
|
||||
<string name="forward_success">Shadowsocks 已啟動。</string>
|
||||
<string name="service_failed">連線至遠端伺服器失敗</string>
|
||||
<string name="nat_deprecated">警告:自 Android 5.0 開始, NAT 模式已過時</string>
|
||||
<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="close">關閉</string>
|
||||
<string name="proxy_empty">Proxy 或密碼不可以空白</string>
|
||||
<string name="connect">連線</string>
|
||||
<string name="initializing">起始中……</string>
|
||||
<string name="recovering">重設中……</string>
|
||||
<string name="loading">載入中……</string>
|
||||
<string name="profiles">設定檔</string>
|
||||
<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 模式可用</string>
|
||||
<string name="flush_dnscache_summary_root">僅在 NAT 模式可用,需要 ROOT 權限</string>
|
||||
<string name="flush_dnscache_no_root">在無 ROOT 權限時清除 DNS 快取無法進行</string>
|
||||
<string name="flushing">清除中……</string>
|
||||
<string name="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">使用 NAT 模式代替 VPN 模式,需要 ROOT 權限</string>
|
||||
|
||||
<string name="udp_dns">UDP 轉送</string>
|
||||
<string name="udp_dns_summary">向遠端轉送 UDP 封包</string>
|
||||
|
||||
<string name="onetime_auth">單次驗證</string>
|
||||
<string name="onetime_auth_summary">啟用單次驗證 (試驗性)</string>
|
||||
|
||||
<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>
|
||||
<string name="connection_test_testing">測試中……</string>
|
||||
<string name="connection_test_available">成功: %d 毫秒延遲</string>
|
||||
<string name="connection_test_fail">無法使用網際網路</string>
|
||||
<string name="connection_test_error">偵測出網際網路連線失敗: %s</string>
|
||||
<string name="connection_test_error_status_code">錯誤碼: #%d</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_manager_dialog">設定檔</string>
|
||||
<string name="profile_manager_dialog_content">• 輕觸選擇一個設定檔;\n• 向左/右撥動或按左移除;\n•
|
||||
按住後上下撥動設定檔重新排序。</string>
|
||||
<string name="gotcha">我懂了!</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-array name="route_entry">
|
||||
<item>全域</item>
|
||||
<item>略過區域網路位址</item>
|
||||
<item>略過中國大陸位址</item>
|
||||
<item>略過區域網路及中國大陸位址</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_export">匯出至剪貼簿</string>
|
||||
<string name="action_import">從剪貼簿匯入</string>
|
||||
<string name="action_export_msg">匯出成功</string>
|
||||
<string name="action_export_err">匯出失敗</string>
|
||||
<string name="action_import_msg">匯入成功</string>
|
||||
<string name="action_import_err">匯入失敗</string>
|
||||
|
||||
<plurals name="removed">
|
||||
<item quantity="one">已移除</item>
|
||||
<item quantity="other">已移除 %d 項</item>
|
||||
</plurals>
|
||||
<string name="undo">復原</string>
|
||||
<string name="toggle_service_state">啟動服務</string>
|
||||
<string name="start_service_default">連線至目前的伺服器</string>
|
||||
<string name="start_service">連線至 %s</string>
|
||||
<string name="stop_service">切換至 %s</string>
|
||||
<string name="profile_default">使用目前的設定檔</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">傳送:</string>
|
||||
<string name="received">接收:</string>
|
||||
|
||||
</resources>
|
||||
@@ -2,10 +2,9 @@
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Shadowsocks</string>
|
||||
<string name="screen_name" translatable="false">shadowsocks</string>
|
||||
<string name="screen_name" translatable="false">shadowsocks ▾</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="global_cat">Global Settings</string>
|
||||
<string name="misc_cat">Misc</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="profile_summary">Switch to another profile or add new profiles</string>
|
||||
@@ -69,11 +68,11 @@
|
||||
<string name="nat_deprecated">WARNING: NAT mode has been deprecated since Android 5.0</string>
|
||||
<string name="nat_no_root">NAT mode requires ROOT permission</string>
|
||||
<string name="switch_to_vpn">Switch to VPN mode</string>
|
||||
<string name="stop">Stop the service</string>
|
||||
<string name="stop">Stop</string>
|
||||
<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">Quick Switch</string>
|
||||
<string name="quick_switch">Switch</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">Close</string>
|
||||
@@ -92,9 +91,9 @@
|
||||
<string name="about">About</string>
|
||||
<string name="about_title">Shadowsocks %s</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="share_message">Scan QRCode or Use NFC (Android Beam)</string>
|
||||
<string name="share_message_without_nfc">Scan QRCode</string>
|
||||
<string name="share_message_nfc_disabled">Scan QRCode, NFC (Android Beam) is disabled</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 & Android Beam</string>
|
||||
<string name="flush_dnscache">Flush DNS cache</string>
|
||||
<string name="flush_dnscache_summary">Only useful in NAT mode</string>
|
||||
@@ -111,12 +110,14 @@
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_manager_dialog">Profiles</string>
|
||||
<string name="profile_manager_dialog_content">• Tap to select a profile;\n• Swipe to remove;\n•
|
||||
Long press to reorder profiles.</string>
|
||||
<string name="profile_manager_dialog_content">• Tap to select a profile;\n• Swipe left/right or press left
|
||||
to remove;\n• Long press to reorder profiles.</string>
|
||||
<string name="gotcha">Gotcha!</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="copy_url">Copy URL</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">Removed</item>
|
||||
|
||||
@@ -2,19 +2,6 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res/com.github.shadowsocks">
|
||||
|
||||
<PreferenceCategory android:title="@string/global_cat">
|
||||
|
||||
<Preference android:key="profiles" android:summary="@string/profile_summary" android:title="@string/profiles">
|
||||
<intent android:action="com.github.shadowsocks.ProfileManagerActivity"/>
|
||||
</Preference>
|
||||
|
||||
<com.github.shadowsocks.preferences.StatusPreference
|
||||
android:key="stat"
|
||||
android:summary="@string/connection_test_pending"
|
||||
android:title="@string/stat"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/proxy_cat">
|
||||
|
||||
|
||||
@@ -44,18 +44,13 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import android.Manifest.permission
|
||||
import android.content._
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.view._
|
||||
import android.widget.AbsListView.OnScrollListener
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget._
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
@@ -95,39 +90,56 @@ object AppManager {
|
||||
}
|
||||
}
|
||||
|
||||
class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnClickListener
|
||||
with OnMenuItemClickListener {
|
||||
class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
import AppManager._
|
||||
|
||||
private var apps: Array[ProxiedApp] = _
|
||||
private var proxiedApps: mutable.HashSet[String] = _
|
||||
private var toolbar: Toolbar = _
|
||||
private var appListView: ListView = _
|
||||
private var loadingView: View = _
|
||||
private var overlay: TextView = _
|
||||
private var adapter: ListAdapter = _
|
||||
private val appsLoading = new AtomicBoolean
|
||||
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
private val icon = itemView.findViewById(R.id.itemicon).asInstanceOf[ImageView]
|
||||
private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
|
||||
private var app: ProxiedApp = _
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
private def setProxied(pn: String, proxied: Boolean) = if (proxied) proxiedApps.add(pn) else proxiedApps.remove(pn)
|
||||
private def proxied = proxiedApps.contains(app.packageName)
|
||||
|
||||
/** Called an application is check/unchecked */
|
||||
def onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
val app: ProxiedApp = buttonView.getTag.asInstanceOf[ProxiedApp]
|
||||
if (app != null) setProxied(app.packageName, isChecked)
|
||||
saveAppSettings(this)
|
||||
}
|
||||
def bind(app: ProxiedApp) {
|
||||
this.app = app
|
||||
icon.setImageDrawable(app.icon)
|
||||
check.setText(app.name)
|
||||
check.setChecked(proxied)
|
||||
}
|
||||
|
||||
def onClick(v: View) {
|
||||
val switch = v.getTag.asInstanceOf[ListEntry].switch
|
||||
val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp]
|
||||
if (app != null) {
|
||||
val proxied = !proxiedApps.contains(app.packageName)
|
||||
setProxied(app.packageName, proxied)
|
||||
switch.setChecked(proxied)
|
||||
def onClick(v: View) {
|
||||
if (proxied) {
|
||||
proxiedApps.remove(app.packageName)
|
||||
check.setChecked(false)
|
||||
} else {
|
||||
proxiedApps.add(app.packageName)
|
||||
check.setChecked(true)
|
||||
}
|
||||
if (!appsLoading.get)
|
||||
ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
|
||||
}
|
||||
saveAppSettings(this)
|
||||
}
|
||||
|
||||
private final class AppsAdapter extends RecyclerView.Adapter[AppViewHolder] {
|
||||
private val apps = getApps(getPackageManager).sortWith((a, b) => {
|
||||
val aProxied = proxiedApps.contains(a.packageName)
|
||||
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
|
||||
})
|
||||
|
||||
def getItemCount = apps.length
|
||||
def onBindViewHolder(vh: AppViewHolder, i: Int) = vh.bind(apps(i))
|
||||
def onCreateViewHolder(vg: ViewGroup, i: Int) =
|
||||
new AppViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_apps_item, vg, false))
|
||||
}
|
||||
|
||||
private val proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
|
||||
private var toolbar: Toolbar = _
|
||||
private var appListView: RecyclerView = _
|
||||
private var loadingView: View = _
|
||||
private val appsLoading = new AtomicBoolean
|
||||
private var handler: Handler = null
|
||||
|
||||
override def onDestroy() {
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
@@ -135,10 +147,6 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
handler = null
|
||||
}
|
||||
if (overlay != null) {
|
||||
getWindowManager.removeViewImmediate(overlay)
|
||||
overlay = null
|
||||
}
|
||||
}
|
||||
|
||||
def onMenuItemClick(item: MenuItem): Boolean = {
|
||||
@@ -160,16 +168,21 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
if (!proxiedAppString.isEmpty) {
|
||||
val editor = prefs.edit
|
||||
val i = proxiedAppString.indexOf('\n')
|
||||
if (i < 0)
|
||||
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
|
||||
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
|
||||
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
// Restart activity
|
||||
appListView.setVisibility(View.GONE)
|
||||
loadingView.setVisibility(View.VISIBLE)
|
||||
reloadApps()
|
||||
return true
|
||||
try {
|
||||
if (i < 0)
|
||||
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
|
||||
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
|
||||
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
// Restart activity
|
||||
appListView.setVisibility(View.GONE)
|
||||
loadingView.setVisibility(View.VISIBLE)
|
||||
reloadApps()
|
||||
return true
|
||||
} catch {
|
||||
case _: IllegalArgumentException =>
|
||||
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,44 +208,23 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
toolbar.inflateMenu(R.menu.app_manager_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
overlay = View.inflate(this, R.layout.overlay, null).asInstanceOf[TextView]
|
||||
getWindowManager.addView(overlay, new
|
||||
WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT))
|
||||
|
||||
ShadowsocksApplication.settings.edit().putBoolean(Key.isProxyApps, true).commit()
|
||||
findViewById(R.id.onSwitch).asInstanceOf[Switch]
|
||||
.setOnCheckedChangeListener((button: CompoundButton, checked: Boolean) => {
|
||||
.setOnCheckedChangeListener((_, checked) => {
|
||||
ShadowsocksApplication.settings.edit().putBoolean(Key.isProxyApps, checked).commit()
|
||||
finish()
|
||||
})
|
||||
|
||||
val bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
|
||||
bypassSwitch.setOnCheckedChangeListener((button: CompoundButton, checked: Boolean) =>
|
||||
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
|
||||
ShadowsocksApplication.settings.edit().putBoolean(Key.isBypassApps, checked).apply())
|
||||
bypassSwitch.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isBypassApps, false))
|
||||
|
||||
loadingView = findViewById(R.id.loading)
|
||||
appListView = findViewById(R.id.applistview).asInstanceOf[ListView]
|
||||
appListView.setOnScrollListener(new AbsListView.OnScrollListener {
|
||||
var visible = false
|
||||
def onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int,
|
||||
totalItemCount: Int) {
|
||||
if (visible) {
|
||||
val name = apps(firstVisibleItem).name
|
||||
overlay.setText(if (name != null && name.length > 1) name(0).toUpper.toString else "*")
|
||||
overlay.setVisibility(View.VISIBLE)
|
||||
}
|
||||
}
|
||||
def onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
visible = true
|
||||
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
|
||||
overlay.setVisibility(View.INVISIBLE)
|
||||
}
|
||||
}
|
||||
})
|
||||
appListView = findViewById(R.id.applistview).asInstanceOf[RecyclerView]
|
||||
appListView.setLayoutManager(new LinearLayoutManager(this))
|
||||
appListView.setItemAnimator(new DefaultItemAnimator)
|
||||
|
||||
instance = this
|
||||
loadAppsAsync()
|
||||
}
|
||||
@@ -241,40 +233,10 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
def loadAppsAsync() {
|
||||
if (!appsLoading.compareAndSet(false, true)) return
|
||||
ThrowableFuture {
|
||||
var adapter: AppsAdapter = null
|
||||
do {
|
||||
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
|
||||
appsLoading.set(true)
|
||||
apps = getApps(getPackageManager).sortWith((a, b) => {
|
||||
val aProxied = proxiedApps.contains(a.packageName)
|
||||
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
|
||||
})
|
||||
adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) {
|
||||
override def getView(position: Int, view: View, parent: ViewGroup): View = {
|
||||
var convertView = view
|
||||
var entry: ListEntry = null
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false)
|
||||
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
|
||||
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
|
||||
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
|
||||
convertView.setOnClickListener(AppManager.this)
|
||||
convertView.setTag(entry)
|
||||
entry.switch.setOnCheckedChangeListener(AppManager.this)
|
||||
} else {
|
||||
entry = convertView.getTag.asInstanceOf[ListEntry]
|
||||
}
|
||||
|
||||
val app: ProxiedApp = apps(position)
|
||||
|
||||
entry.text.setText(app.name)
|
||||
entry.icon.setImageDrawable(app.icon)
|
||||
val switch = entry.switch
|
||||
switch.setTag(app)
|
||||
switch.setChecked(proxiedApps.contains(app.packageName))
|
||||
entry.text.setTag(switch)
|
||||
convertView
|
||||
}
|
||||
}
|
||||
adapter = new AppsAdapter
|
||||
} while (!appsLoading.compareAndSet(true, false))
|
||||
handler.post(() => {
|
||||
appListView.setAdapter(adapter)
|
||||
@@ -283,12 +245,6 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
}
|
||||
|
||||
def saveAppSettings(context: Context) {
|
||||
if (!appsLoading.get) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
|
||||
}
|
||||
|
||||
var handler: Handler = null
|
||||
|
||||
override def onKeyUp(keyCode: Int, event: KeyEvent) = keyCode match {
|
||||
case KeyEvent.KEYCODE_MENU =>
|
||||
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu else toolbar.showOverflowMenu
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.util.{Timer, TimerTask}
|
||||
import android.app.Service
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.{Handler, RemoteCallbackList}
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
|
||||
@@ -104,9 +105,10 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
override def use(config: Config) = synchronized(state match {
|
||||
case State.STOPPED => if (config != null) startRunner(config)
|
||||
case State.STOPPED => if (config != null && checkConfig(config)) startRunner(config)
|
||||
case State.CONNECTED =>
|
||||
if (config == null) stopRunner(true) else if (config.profileId != BaseService.this.config.profileId) {
|
||||
if (config == null) stopRunner(true)
|
||||
else if (config.profileId != BaseService.this.config.profileId && checkConfig(config)) {
|
||||
stopRunner(false)
|
||||
startRunner(config)
|
||||
}
|
||||
@@ -114,6 +116,12 @@ trait BaseService extends Service {
|
||||
})
|
||||
}
|
||||
|
||||
def checkConfig(config: Config) = if (TextUtils.isEmpty(config.proxy) || TextUtils.isEmpty(config.sitekey)) {
|
||||
changeState(State.STOPPED)
|
||||
stopRunner(true)
|
||||
false
|
||||
} else true
|
||||
|
||||
def startRunner(config: Config) {
|
||||
this.config = config
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2014 <max.c.lv@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{DialogInterface, Intent}
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.nfc.{NdefMessage, NfcAdapter}
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.{AlertDialog, AppCompatActivity}
|
||||
import android.text.TextUtils
|
||||
import android.view.WindowManager
|
||||
import com.github.shadowsocks.utils.Parser
|
||||
|
||||
class ParserActivity extends AppCompatActivity {
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handledSharedIntent()
|
||||
}
|
||||
|
||||
override def onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handledSharedIntent()
|
||||
}
|
||||
|
||||
def handledSharedIntent() {
|
||||
val intent = getIntent()
|
||||
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
|
||||
}
|
||||
showAsPopup()
|
||||
val dialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_profile_dialog)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.yes, ((_, _) =>
|
||||
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)): DialogInterface.OnClickListener)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.setMessage(profiles.mkString("\n"))
|
||||
.create()
|
||||
dialog.setOnDismissListener((_: DialogInterface) => finish()) // builder method was added in API 17
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
def showAsPopup() {
|
||||
val window = getWindow
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
val params = window.getAttributes
|
||||
params.alpha = 1.0f
|
||||
params.dimAmount = 0.5f
|
||||
window.setAttributes(params.asInstanceOf[android.view.WindowManager.LayoutParams])
|
||||
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import java.nio.charset.Charset
|
||||
import android.content._
|
||||
import android.nfc.NfcAdapter.CreateNdefMessageCallback
|
||||
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
|
||||
import android.os.{Build, Bundle, Handler}
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.provider.Settings
|
||||
import android.support.v7.app.{AlertDialog, AppCompatActivity}
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||
@@ -15,8 +15,9 @@ import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.text.style.TextAppearanceSpan
|
||||
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
|
||||
import android.view.{LayoutInflater, MenuItem, View, ViewGroup}
|
||||
import android.view._
|
||||
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
|
||||
import com.github.clans.fab.{FloatingActionButton, FloatingActionMenu}
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
|
||||
@@ -26,18 +27,20 @@ import net.glxn.qrgen.android.QRCode
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
|
||||
with CreateNdefMessageCallback {
|
||||
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
|
||||
with View.OnClickListener with CreateNdefMessageCallback {
|
||||
private final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
|
||||
with View.OnClickListener with View.OnKeyListener {
|
||||
var item: Profile = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnKeyListener(this)
|
||||
|
||||
{
|
||||
val shareBtn = itemView.findViewById(R.id.share)
|
||||
shareBtn.setOnClickListener(_ => {
|
||||
val url = item.toString
|
||||
if (nfcBeamEnable) {
|
||||
if (isNfcBeamEnabled) {
|
||||
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this)
|
||||
nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
|
||||
}
|
||||
@@ -56,23 +59,15 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
.setView(image)
|
||||
.setTitle(R.string.share)
|
||||
.create()
|
||||
if (!nfcAvailable){
|
||||
dialog.setMessage(getString(R.string.share_message_without_nfc))
|
||||
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 {
|
||||
if (!nfcBeamEnable) {
|
||||
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
|
||||
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
|
||||
} else {
|
||||
startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||
}
|
||||
): DialogInterface.OnClickListener)
|
||||
} else {
|
||||
dialog.setMessage(getString(R.string.share_message))
|
||||
dialog.setOnDismissListener(_ =>
|
||||
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
|
||||
}
|
||||
dialog.setMessage(getString(R.string.share_message))
|
||||
dialog.setOnDismissListener(_ =>
|
||||
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
|
||||
}
|
||||
dialog.show()
|
||||
})
|
||||
@@ -110,10 +105,19 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
}
|
||||
}
|
||||
|
||||
def onClick(v: View) = {
|
||||
def onClick(v: View) {
|
||||
ShadowsocksApplication.switchProfile(item.id)
|
||||
finish
|
||||
}
|
||||
|
||||
def onKey(v: View, keyCode: Int, event: KeyEvent) = if (event.getAction == KeyEvent.ACTION_UP) keyCode match {
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT =>
|
||||
val index = getAdapterPosition
|
||||
profilesAdapter.remove(index)
|
||||
undoManager.remove(index, item)
|
||||
true
|
||||
case _ => false
|
||||
} else false
|
||||
}
|
||||
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
@@ -170,6 +174,8 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
private var selectedItem: ProfileViewHolder = _
|
||||
private val handler = new Handler
|
||||
|
||||
private var menu : FloatingActionMenu = _
|
||||
|
||||
private lazy val profilesAdapter = new ProfilesAdapter
|
||||
private var undoManager: UndoSnackbarManager[Profile] = _
|
||||
|
||||
@@ -177,9 +183,9 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
|
||||
private var nfcAdapter : NfcAdapter = _
|
||||
private var nfcShareItem: Array[Byte] = _
|
||||
private var nfcAvailable = false
|
||||
private var nfcEnable = false
|
||||
private var nfcBeamEnable = false
|
||||
private var isNfcAvailable: Boolean = _
|
||||
private var isNfcEnabled: Boolean = _
|
||||
private var isNfcBeamEnabled: Boolean = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -195,6 +201,8 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
toolbar.inflateMenu(R.menu.profile_manager_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
initFab()
|
||||
|
||||
ShadowsocksApplication.profileManager.setProfileAddedListener(profilesAdapter.add)
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
val layoutManager = new LinearLayoutManager(this)
|
||||
@@ -229,33 +237,122 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
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 manualAddFAB = findViewById(R.id.fab_manual_add).asInstanceOf[FloatingActionButton]
|
||||
manualAddFAB.setOnClickListener(this)
|
||||
val qrcodeAddFAB = findViewById(R.id.fab_qrcode_add).asInstanceOf[FloatingActionButton]
|
||||
qrcodeAddFAB.setOnClickListener(this)
|
||||
val nfcAddFAB = findViewById(R.id.fab_nfc_add).asInstanceOf[FloatingActionButton]
|
||||
nfcAddFAB.setOnClickListener(this)
|
||||
val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
|
||||
importAddFAB.setOnClickListener(this)
|
||||
}
|
||||
|
||||
|
||||
override def onResume() {
|
||||
super.onResume()
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (nfcAdapter != null){
|
||||
nfcAvailable = true
|
||||
if (nfcAdapter.isEnabled) {
|
||||
nfcEnable = true
|
||||
if (nfcAdapter.isNdefPushEnabled) {
|
||||
nfcBeamEnable = true
|
||||
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
|
||||
updateNfcState()
|
||||
}
|
||||
|
||||
override def onNewIntent(intent: Intent): Unit ={
|
||||
super.onNewIntent(intent)
|
||||
handleShareIntent(intent)
|
||||
}
|
||||
|
||||
override def onClick(v: View){
|
||||
v.getId match {
|
||||
case R.id.fab_manual_add =>
|
||||
menu.toggle(true)
|
||||
ShadowsocksApplication.profileManager.reload(-1)
|
||||
ShadowsocksApplication.switchProfile(ShadowsocksApplication.profileManager.save.id)
|
||||
finish
|
||||
case R.id.fab_qrcode_add =>
|
||||
menu.toggle(false)
|
||||
val integrator = new IntentIntegrator(ProfileManagerActivity.this)
|
||||
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
|
||||
list.add("tw.com.quickmark")
|
||||
integrator.setTargetApplications(list)
|
||||
integrator.initiateScan()
|
||||
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 {
|
||||
nfcBeamEnable = false
|
||||
dialog.setMessage(getString(R.string.add_profile_nfc_hint))
|
||||
}
|
||||
} else{
|
||||
nfcEnable = false
|
||||
nfcBeamEnable = false
|
||||
}
|
||||
} else {
|
||||
nfcAvailable = false
|
||||
nfcEnable = false
|
||||
nfcBeamEnable = false
|
||||
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(ShadowsocksApplication.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(ShadowsocksApplication.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
|
||||
@@ -281,33 +378,14 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
}
|
||||
}
|
||||
|
||||
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.scan_qr_code =>
|
||||
val integrator = new IntentIntegrator(this)
|
||||
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
|
||||
list.add("tw.com.quickmark")
|
||||
integrator.setTargetApplications(list)
|
||||
integrator.initiateScan()
|
||||
true
|
||||
case R.id.manual_settings =>
|
||||
ShadowsocksApplication.profileManager.reload(-1)
|
||||
ShadowsocksApplication.switchProfile(ShadowsocksApplication.profileManager.save.id)
|
||||
finish
|
||||
true
|
||||
case R.id.action_import =>
|
||||
if (clipboard.hasPrimaryClip) {
|
||||
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
|
||||
if (profiles.nonEmpty) {
|
||||
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show
|
||||
return true
|
||||
}
|
||||
}
|
||||
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show
|
||||
true
|
||||
case R.id.action_export =>
|
||||
ShadowsocksApplication.profileManager.getAllProfiles match {
|
||||
case Some(profiles) =>
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.io.{FileOutputStream, IOException, InputStream, OutputStream}
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.net.{HttpURLConnection, URL}
|
||||
import java.util
|
||||
import java.util.Locale
|
||||
|
||||
@@ -59,6 +61,7 @@ import android.widget._
|
||||
import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database._
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
|
||||
|
||||
@@ -105,7 +108,6 @@ class Shadowsocks
|
||||
var progressTag = -1
|
||||
var state = State.STOPPED
|
||||
var currentProfile = new Profile
|
||||
var vpnEnabled = -1
|
||||
|
||||
// Services
|
||||
var currentServiceName = classOf[ShadowsocksNatService].getName
|
||||
@@ -119,6 +121,7 @@ class Shadowsocks
|
||||
fab.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
preferences.setEnabled(false)
|
||||
stat.setVisibility(View.GONE)
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
if (state == State.CONNECTING) {
|
||||
@@ -129,6 +132,11 @@ class Shadowsocks
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = true)
|
||||
preferences.setEnabled(false)
|
||||
stat.setVisibility(View.VISIBLE)
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
connectionTestText.setVisibility(View.VISIBLE)
|
||||
connectionTestText.setText(getString(R.string.connection_test_pending))
|
||||
} else connectionTestText.setVisibility(View.GONE)
|
||||
case State.STOPPED =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fabProgressCircle.postDelayed(hideCircle, 1000)
|
||||
@@ -143,12 +151,14 @@ class Shadowsocks
|
||||
Log.e(Shadowsocks.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)
|
||||
}
|
||||
state = s
|
||||
})
|
||||
@@ -158,9 +168,12 @@ class Shadowsocks
|
||||
}
|
||||
}
|
||||
|
||||
def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) = preferences.stat
|
||||
.setRate(TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal),
|
||||
TrafficMonitor.formatTraffic(txRate) + "/s", TrafficMonitor.formatTraffic(rxRate) + "/s")
|
||||
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")
|
||||
}
|
||||
|
||||
def attachService: Unit = attachService(callback)
|
||||
|
||||
@@ -202,11 +215,18 @@ class Shadowsocks
|
||||
if (fab != null) fab.setEnabled(false)
|
||||
}
|
||||
|
||||
private lazy val preferences =
|
||||
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
|
||||
private var adView: AdView = _
|
||||
private var testCount: Int = _
|
||||
private lazy val stat = findViewById(R.id.stat)
|
||||
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]
|
||||
|
||||
var handler = new Handler()
|
||||
|
||||
@@ -350,13 +370,67 @@ class Shadowsocks
|
||||
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
|
||||
val field = classOf[Toolbar].getDeclaredField("mTitleTextView")
|
||||
field.setAccessible(true)
|
||||
val title: TextView = field.get(toolbar).asInstanceOf[TextView]
|
||||
val tf: Typeface = Typefaces.get(this, "fonts/Iceland.ttf")
|
||||
val title = field.get(toolbar).asInstanceOf[TextView]
|
||||
title.setFocusable(true)
|
||||
title.setOnClickListener(_ => startActivity(new Intent(this, classOf[ProfileManagerActivity])))
|
||||
val typedArray = obtainStyledAttributes(Array(R.attr.selectableItemBackgroundBorderless))
|
||||
title.setBackgroundResource(typedArray.getResourceId(0, 0))
|
||||
typedArray.recycle
|
||||
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
|
||||
if (tf != null) title.setTypeface(tf)
|
||||
|
||||
connectionTestText = findViewById(R.id.connection_test).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]
|
||||
connectionTestText.setFocusable(true)
|
||||
connectionTestText.setOnClickListener(_ => {
|
||||
val id = synchronized {
|
||||
testCount += 1
|
||||
handler.post(() => connectionTestText.setText(R.string.connection_test_testing))
|
||||
testCount
|
||||
}
|
||||
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()
|
||||
.asInstanceOf[HttpURLConnection]) { conn =>
|
||||
conn.setConnectTimeout(5 * 1000)
|
||||
conn.setReadTimeout(5 * 1000)
|
||||
conn.setInstanceFollowRedirects(false)
|
||||
conn.setUseCaches(false)
|
||||
if (testCount == id) {
|
||||
var result: String = null
|
||||
var success = true
|
||||
try {
|
||||
val start = currentTimeMillis
|
||||
conn.getInputStream
|
||||
val elapsed = currentTimeMillis - start
|
||||
val code = conn.getResponseCode
|
||||
if (code == 204 || code == 200 && conn.getContentLength == 0)
|
||||
result = getString(R.string.connection_test_available, elapsed: java.lang.Long)
|
||||
else throw new Exception(getString(R.string.connection_test_error_status_code, code: Integer))
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
success = false
|
||||
result = getString(R.string.connection_test_error, e.getMessage)
|
||||
}
|
||||
synchronized(if (testCount == id) {
|
||||
if (ShadowsocksApplication.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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
|
||||
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
|
||||
fab.setOnClickListener((v: View) => if (serviceStarted) serviceStop()
|
||||
fab.setOnClickListener(_ => if (serviceStarted) serviceStop()
|
||||
else if (checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null) prepareStartService()
|
||||
else changeSwitch(checked = false))
|
||||
fab.setOnLongClickListener((v: View) => {
|
||||
@@ -391,24 +465,32 @@ class Shadowsocks
|
||||
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 (ShadowsocksApplication.isVpnEnabled) {
|
||||
connectionTestText.setVisibility(View.VISIBLE)
|
||||
connectionTestText.setText(getString(R.string.connection_test_pending))
|
||||
} else connectionTestText.setVisibility(View.GONE)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks])
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
|
||||
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, service.getString(R.string.stop),
|
||||
.addAction(R.drawable.ic_navigation_close, service.getString(R.string.stop),
|
||||
PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
|
||||
ShadowsocksApplication.profileManager.getAllProfiles match {
|
||||
case Some(profiles) => if (profiles.length > 1)
|
||||
builder.addAction(android.R.drawable.ic_menu_manage, service.getString(R.string.quick_switch),
|
||||
builder.addAction(R.drawable.ic_action_settings, service.getString(R.string.quick_switch),
|
||||
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.net.{HttpURLConnection, URL}
|
||||
import java.util.Locale
|
||||
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.{DialogInterface, Intent, SharedPreferences}
|
||||
import android.content.{Intent, SharedPreferences}
|
||||
import android.net.Uri
|
||||
import android.os.{Build, Bundle}
|
||||
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.webkit.{WebView, WebViewClient}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.preferences._
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
object ShadowsocksSettings {
|
||||
@@ -69,62 +65,14 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
|
||||
private def activity = getActivity.asInstanceOf[Shadowsocks]
|
||||
lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
|
||||
var stat: StatusPreference = _
|
||||
|
||||
private var isProxyApps: SwitchPreference = _
|
||||
private var testCount: Int = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
addPreferencesFromResource(R.xml.pref_all)
|
||||
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
stat = findPreference(Key.stat).asInstanceOf[StatusPreference]
|
||||
stat.setOnPreferenceClickListener(_ => {
|
||||
val id = synchronized {
|
||||
testCount += 1
|
||||
activity.handler.post(() => stat.setSummary(R.string.connection_test_testing))
|
||||
testCount
|
||||
}
|
||||
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()
|
||||
.asInstanceOf[HttpURLConnection]) { conn =>
|
||||
conn.setConnectTimeout(5 * 1000)
|
||||
conn.setReadTimeout(5 * 1000)
|
||||
conn.setInstanceFollowRedirects(false)
|
||||
conn.setUseCaches(false)
|
||||
if (testCount == id) {
|
||||
var result: String = null
|
||||
var success = true
|
||||
try {
|
||||
val start = currentTimeMillis
|
||||
conn.getInputStream
|
||||
val elapsed = currentTimeMillis - start
|
||||
val code = conn.getResponseCode
|
||||
if (code == 204 || code == 200 && conn.getContentLength == 0)
|
||||
result = getString(R.string.connection_test_available, elapsed: java.lang.Long)
|
||||
else throw new Exception(getString(R.string.connection_test_error_status_code, code: Integer))
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
success = false
|
||||
result = getString(R.string.connection_test_error, e.getMessage)
|
||||
}
|
||||
synchronized(if (testCount == id) {
|
||||
if (ShadowsocksApplication.isVpnEnabled) activity.handler.post(() => {
|
||||
if (success) stat.setSummary(result) else {
|
||||
stat.setSummary(R.string.connection_test_fail)
|
||||
Snackbar.make(activity.findViewById(android.R.id.content), result, Snackbar.LENGTH_LONG).show
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
stat.setSummary(if (ShadowsocksApplication.isVpnEnabled) getString(R.string.connection_test_pending) else null)
|
||||
|
||||
isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference]
|
||||
isProxyApps.setOnPreferenceClickListener((preference: Preference) => {
|
||||
startActivity(new Intent(activity, classOf[AppManager]))
|
||||
@@ -163,9 +111,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, ShadowsocksApplication.getVersionName))
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(getString(android.R.string.ok),
|
||||
((dialog: DialogInterface, id: Int) => dialog.cancel()): DialogInterface.OnClickListener)
|
||||
.setNegativeButton(getString(android.R.string.ok), null)
|
||||
.setView(web)
|
||||
.create()
|
||||
.show()
|
||||
@@ -189,7 +135,6 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
activity.deattachService
|
||||
activity.attachService
|
||||
})
|
||||
stat.setSummary(if (ShadowsocksApplication.isVpnEnabled) getString(R.string.connection_test_pending) else null)
|
||||
case _ =>
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ import android.os.{Build, Bundle}
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import android.widget.{Switch, CheckedTextView}
|
||||
import android.widget.{CheckedTextView, Switch}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
import com.github.shadowsocks.utils.TaskerSettings
|
||||
|
||||
/**
|
||||
* @author CzBiX
|
||||
@@ -103,7 +103,7 @@ class TaskerActivity extends AppCompatActivity {
|
||||
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(R.string.app_name)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
|
||||
toolbar.setNavigationOnClickListener(_ => finish())
|
||||
|
||||
taskerOption = TaskerSettings.fromIntent(getIntent)
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
import com.github.shadowsocks.utils.{TaskerSettings, Utils}
|
||||
|
||||
/**
|
||||
* @author CzBiX
|
||||
|
||||
@@ -99,6 +99,6 @@ class Profile {
|
||||
@DatabaseField
|
||||
var userOrder: Long = _
|
||||
|
||||
override def toString = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
|
||||
method, password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP)
|
||||
override def toString = "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)
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.Preference
|
||||
import android.util.AttributeSet
|
||||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import android.widget.TextView
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* @author madeye
|
||||
*/
|
||||
final class StatusPreference(context: Context, attrs: AttributeSet = null)
|
||||
extends Preference(context, attrs) {
|
||||
var tx: String = _
|
||||
var rx: String = _
|
||||
var txRate: String = _
|
||||
var rxRate: String = _
|
||||
var txView: TextView = _
|
||||
var rxView: TextView = _
|
||||
var txRateView: TextView = _
|
||||
var rxRateView: TextView = _
|
||||
|
||||
override def onCreateView(parent: ViewGroup): View = {
|
||||
val view = getContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
|
||||
.inflate(R.layout.status_pref, parent, false)
|
||||
|
||||
txView = view.findViewById(R.id.tx).asInstanceOf[TextView]
|
||||
rxView = view.findViewById(R.id.rx).asInstanceOf[TextView]
|
||||
txRateView = view.findViewById(R.id.txRate).asInstanceOf[TextView]
|
||||
rxRateView = view.findViewById(R.id.rxRate).asInstanceOf[TextView]
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
override def onBindView(view: View) = {
|
||||
super.onBindView(view)
|
||||
txView.setText(tx)
|
||||
rxView.setText(rx)
|
||||
txRateView.setText(txRate)
|
||||
rxRateView.setText(rxRate)
|
||||
}
|
||||
|
||||
def setRate(tx: String, rx: String, txRate: String, rxRate: String) {
|
||||
this.tx = tx
|
||||
if (txView != null) {
|
||||
txView.setText(tx)
|
||||
}
|
||||
this.rx = rx
|
||||
if (rxView != null) {
|
||||
rxView.setText(rx)
|
||||
}
|
||||
this.txRate = txRate
|
||||
if (txRateView != null) {
|
||||
txRateView.setText(txRate)
|
||||
}
|
||||
this.rxRate = rxRate
|
||||
if (rxRateView != null) {
|
||||
rxRateView.setText(rxRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,6 @@ object Key {
|
||||
|
||||
val proxied = "Proxyed"
|
||||
|
||||
val profiles = "profiles"
|
||||
val isNAT = "isNAT"
|
||||
val route = "route"
|
||||
val stat = "stat"
|
||||
|
||||
@@ -45,22 +45,22 @@ import com.github.shadowsocks.database.Profile
|
||||
object Parser {
|
||||
val TAG = "ShadowParser"
|
||||
private val pattern = "(?i)ss://([A-Za-z0-9+-/=_]+)".r
|
||||
private val decodedPattern = "(?i)^((.+?)(-auth)??:(.*)@(.+?):(\\d+?))$".r
|
||||
|
||||
def findAll(data: CharSequence) = pattern.findAllMatchIn(data).map(m => try {
|
||||
val content = new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")
|
||||
val methodIdx = content.indexOf(':')
|
||||
val passwordIdx = content.lastIndexOf('@')
|
||||
val hostIdx = content.lastIndexOf(':')
|
||||
val host = content.substring(passwordIdx + 1, hostIdx)
|
||||
|
||||
val profile = new Profile
|
||||
profile.name = host
|
||||
profile.host = host
|
||||
profile.remotePort = content.substring(hostIdx + 1).toInt
|
||||
profile.method = content.substring(0, methodIdx).toLowerCase
|
||||
profile.password = content.substring(methodIdx + 1, passwordIdx)
|
||||
profile
|
||||
} catch {
|
||||
def findAll(data: CharSequence) = pattern.findAllMatchIn(data).map(m => try
|
||||
decodedPattern.findFirstMatchIn(new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")) match {
|
||||
case Some(ss) =>
|
||||
val profile = new Profile
|
||||
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.remotePort = ss.group(6).toInt
|
||||
profile
|
||||
case _ => null
|
||||
}
|
||||
catch {
|
||||
case ex: Exception =>
|
||||
Log.e(TAG, "parser error: " + m.source, ex)// Ignore
|
||||
null
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
package com.github.shadowsocks.helper
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.content.{Context, Intent}
|
||||
import android.os.Bundle
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.github.shadowsocks.widget
|
||||
|
||||
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.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class AutoPaddingBehavior(context: Context, attrs: AttributeSet) extends Behavior[View] {
|
||||
override def layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View) =
|
||||
dependency.isInstanceOf[SnackbarLayout]
|
||||
|
||||
override def onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View) =
|
||||
dependency match {
|
||||
case sl: SnackbarLayout =>
|
||||
child.setPadding(0, 0, 0, dependency.getHeight)
|
||||
true
|
||||
case _ => super.onDependentViewChanged(parent, child, dependency)
|
||||
}
|
||||
|
||||
override def onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) =
|
||||
dependency match {
|
||||
case sl: SnackbarLayout => child.setPadding(0, 0, 0, 0)
|
||||
case _ => super.onDependentViewRemoved(parent, child, dependency)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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._
|
||||
|
||||
/**
|
||||
* Behavior for com.github.clans.fab.FloatingActionMenu that is aware of Snackbars and scrolling.
|
||||
*
|
||||
* @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) =
|
||||
dependency.isInstanceOf[SnackbarLayout]
|
||||
|
||||
override def onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionMenu, dependency: View) = {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
|
||||
private val removedCallback = new Snackbar.Callback {
|
||||
override def onDismissed(snackbar: Snackbar, event: Int) = {
|
||||
event match {
|
||||
case Snackbar.Callback.DISMISS_EVENT_MANUAL | Snackbar.Callback.DISMISS_EVENT_TIMEOUT =>
|
||||
case Snackbar.Callback.DISMISS_EVENT_SWIPE | Snackbar.Callback.DISMISS_EVENT_MANUAL |
|
||||
Snackbar.Callback.DISMISS_EVENT_TIMEOUT =>
|
||||
if (commit != null) commit(recycleBin.iterator)
|
||||
recycleBin.clear
|
||||
case _ =>
|
||||
|
||||
Reference in New Issue
Block a user