Commit dbb137c5 authored by Mygod's avatar Mygod

Initial redesign draft for #762

parent 7eddf8be
...@@ -31,17 +31,18 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW") ...@@ -31,17 +31,18 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
resolvers += Resolver.jcenterRepo resolvers += Resolver.jcenterRepo
libraryDependencies ++= libraryDependencies ++=
"com.android.support" % "cardview-v7" % "25.1.0" ::
"com.android.support" % "design" % "25.1.0" :: "com.android.support" % "design" % "25.1.0" ::
"com.android.support" % "gridlayout-v7" % "25.1.0" :: "com.android.support" % "gridlayout-v7" % "25.1.0" ::
"com.android.support" % "preference-v14" % "25.1.0" :: "com.android.support" % "preference-v14" % "25.1.0" ::
"com.evernote" % "android-job" % "1.1.3" :: "com.evernote" % "android-job" % "1.1.3" ::
"com.github.clans" % "fab" % "1.6.4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" :: "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.github.kevinsawicki" % "http-request" % "6.0" :: "com.github.kevinsawicki" % "http-request" % "6.0" ::
"com.google.android.gms" % "play-services-ads" % "10.0.1" :: "com.google.android.gms" % "play-services-ads" % "10.0.1" ::
"com.google.android.gms" % "play-services-analytics" % "10.0.1" :: "com.google.android.gms" % "play-services-analytics" % "10.0.1" ::
"com.google.android.gms" % "play-services-gcm" % "10.0.1" :: "com.google.android.gms" % "play-services-gcm" % "10.0.1" ::
"com.j256.ormlite" % "ormlite-android" % "5.0" :: "com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "materialdrawer" % "5.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" :: "com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.7" :: "dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" :: "eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/> android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<activity <activity
android:name=".Shadowsocks" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
...@@ -49,28 +49,6 @@ ...@@ -49,28 +49,6 @@
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ShadowsocksRunnerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask">
</activity>
<activity
android:name=".ProfileManagerActivity"
android:label="@string/profiles"
android:parentActivityName=".Shadowsocks"
android:excludeFromRecents="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.github.shadowsocks.ProfileManagerActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
...@@ -82,18 +60,34 @@ ...@@ -82,18 +60,34 @@
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" /> <data android:scheme="ss" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity> </activity>
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="Profile config"
android:launchMode="singleTask"
android:parentActivityName=".MainActivity"/>
<activity
android:name=".ShadowsocksRunnerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
<activity <activity
android:name=".ScannerActivity" android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code" android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".ProfileManagerActivity" android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/> android:excludeFromRecents="true"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
android:parentActivityName=".Shadowsocks" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
......
package com.github.shadowsocks;
import android.app.Activity;
/**
* This helper class is used to solve Java-Scala interop problem. Use onAttach(Context) and remove this class
* when minSdkVersion >= 23.
*
* @author Mygod
*/
public class DialogFragment extends android.app.DialogFragment {
protected void superOnAttach(Activity activity) {
super.onAttach(activity);
}
}
<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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector> </vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<fragment android:id="@+id/content"
class="com.github.shadowsocks.GlobalConfigFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
...@@ -3,17 +3,23 @@ ...@@ -3,17 +3,23 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" 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" android:layout_width="match_parent" android:layout_height="match_parent"
tools:ignore="UnknownIdInLayout" android:clipChildren="false"> tools:ignore="UnknownIdInLayout" android:clipChildren="false">
<LinearLayout android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" <FrameLayout android:id="@+id/content"
android:orientation="vertical" android:layout_below="@+id/toolbar" android:layout_width="match_parent"
android:animateLayoutChanges="true"> android:layout_height="match_parent"
<include layout="@layout/toolbar_light_dark" /> android:layout_above="@+id/stat"/>
<LinearLayout android:id="@+id/stat" android:elevation="4dp" android:visibility="gone" <LinearLayout android:id="@+id/stat"
android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_width="match_parent"
android:minHeight="?android:attr/listPreferredItemHeight" android:gravity="center_vertical" android:layout_height="wrap_content"
android:paddingStart="16dip" android:paddingEnd="16dip" android:focusable="true" android:elevation="4dp"
android:paddingTop="16dip" android:paddingBottom="16dip" android:paddingStart="16dip"
android:background="@drawable/background_stat" android:orientation="vertical" android:paddingEnd="16dip"
android:nextFocusRight="@+id/fab"> android:paddingTop="16dip"
android:paddingBottom="16dip"
android:focusable="true"
android:background="@drawable/background_stat"
android:orientation="vertical"
android:nextFocusRight="@+id/fab"
android:layout_alignParentBottom="true">
<LinearLayout android:layout_width="match_parent" <LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView android:text="@string/stat" <TextView android:text="@string/stat"
...@@ -69,17 +75,15 @@ ...@@ -69,17 +75,15 @@
app:layout_gravity="fill_horizontal" android:gravity="end"/> app:layout_gravity="fill_horizontal" android:gravity="end"/>
</android.support.v7.widget.GridLayout> </android.support.v7.widget.GridLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> <com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle"
<fragment android:name="com.github.shadowsocks.ShadowsocksSettings" app:reusable="true"
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_width="wrap_content"
android:layout_height="wrap_content" android:elevation="6dp" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/title_bar" android:elevation="6dp"
android:layout_alignTop="@+id/stat"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/fab_margin_bottom"> android:layout_marginTop="@dimen/fab_margin_top">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:backgroundTint="@color/material_blue_grey_700" app:backgroundTint="@color/material_blue_grey_700"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<fragment android:id="@+id/content"
class="com.github.shadowsocks.ProfileConfigFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<android.support.design.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="0dp" android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/menu"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="bottom|end" android:orientation="vertical">
android:layout_marginBottom="16dp" <include layout="@layout/toolbar_light_dark"/>
android:layout_marginEnd="16dp" <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
app:menu_colorNormal="@color/material_primary_500" android:id="@+id/profilesList"
app:menu_colorPressed="@color/material_primary_700" android:layout_width="match_parent"
app:fab_colorRipple="@android:color/white" android:layout_height="0dp"
app:layout_behavior="com.github.shadowsocks.widget.FloatingActionMenuBehavior"> android:layout_weight="1"
android:paddingBottom="8dp"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_manual_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_manual_settings"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_qrcode_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_scan_qr_code"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_nfc_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="NFC"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_import_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/action_import"
/>
</com.github.clans.fab.FloatingActionMenu>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/master/core/res/res/layout/select_dialog_singlechoice_material.xml --> <!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/container"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:focusable="true" android:focusable="true"
android:nextFocusRight="@+id/share"> android:nextFocusRight="@+id/edit"
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_marginTop="8dp"
android:id="@android:id/text1" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="8dp">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/container">
<TextView android:id="@android:id/text1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_weight="1" android:layout_weight="1"
android:minHeight="?android:attr/listPreferredItemHeightSmall" android:textAppearance="?android:attr/textAppearanceLarge"
android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="?android:attr/textColorPrimary"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:gravity="center_vertical" android:gravity="center_vertical"
android:drawableStart="?android:attr/listChoiceIndicatorSingle" android:maxLines="2"
android:drawablePadding="20dp" android:ellipsize="end"/>
android:maxLines="4" <ImageView android:id="@+id/edit"
android:scrollHorizontally="false" />
<ImageView android:id="@+id/share"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="5dp" android:padding="4dp"
android:layout_gravity="center_vertical" android:layout_gravity="top"
app:srcCompat="@drawable/ic_social_share" app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share" android:contentDescription="Edit"
android:focusable="true" android:focusable="true"
android:nextFocusLeft="@+id/container"/> android:nextFocusLeft="@+id/container"/>
</LinearLayout> </LinearLayout>
<TextView android:id="@android:id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
</android.support.v7.widget.CardView>
<?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/share"
android:icon="@drawable/abc_ic_menu_share_mtrl_alpha"
android:alphabeticShortcut="s"
android:numericShortcut="1"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_qr_code_nfc"
android:title="QR code/NFC"
android:alphabeticShortcut="q"
android:numericShortcut="1"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:alphabeticShortcut="e"
android:numericShortcut="2"/>
</menu>
</item>
<item android:id="@+id/action_delete"
android:title="Delete"
android:icon="@drawable/ic_action_delete"
android:alphabeticShortcut="d"
android:numericShortcut="2"
app:showAsAction="always"/>
<item android:id="@+id/action_apply"
android:title="Apply"
android:icon="@drawable/ic_action_done"
android:alphabeticShortcut="a"
android:numericShortcut="3"
app:showAsAction="always"/>
</menu>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_profile"
android:icon="@drawable/ic_content_add"
android:alphabeticShortcut="a"
android:numericShortcut="0"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_scan_qr_code"
android:title="@string/add_profile_methods_scan_qr_code"
android:alphabeticShortcut="s"
android:numericShortcut="1"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="2"/>
<item android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="3"/>
</menu>
</item>
<item android:id="@+id/action_export" <item android:id="@+id/action_export"
android:title="@string/action_export" android:title="@string/action_export"
android:alphabeticShortcut="e" android:alphabeticShortcut="e"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="fab_margin_bottom">-28dp</dimen> <dimen name="fab_margin_top">-28dp</dimen>
</resources> </resources>
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
<resources> <resources>
<color name="material_green_700">#388E3C</color> <color name="material_green_700">#388E3C</color>
<color name="material_green_a700">#00C853</color>
<color name="material_blue_grey_300">#90A4AE</color> <color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color> <color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</color> <color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_300">@color/material_blue_grey_300</color> <color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color> <color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_700">@color/material_blue_grey_700</color> <color name="material_primary_700">@color/material_blue_grey_700</color>
<color name="material_accent_200">@color/material_blue_grey_500</color> <color name="material_accent_200">@color/material_green_a700</color>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 --> <!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 -->
<dimen name="fab_margin_bottom">-44dp</dimen> <dimen name="fab_margin_top">-44dp</dimen>
</resources> </resources>
...@@ -6,11 +6,11 @@ ...@@ -6,11 +6,11 @@
<item name="android:textSize">24sp</item> <item name="android:textSize">24sp</item>
</style> </style>
<style name="Theme.MaterialBase" parent="Theme.AppCompat.Light.NoActionBar"> <style name="Theme.MaterialBase" parent="MaterialDrawerTheme.Light.DarkToolbar">
<!--suppress NewApi --> <!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item> <item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="colorAccent">@color/material_primary_500</item> <item name="colorAccent">@color/material_primary_500</item>
<item name="colorButtonNormal">@color/material_primary_500</item> <item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item> <item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item> <item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
......
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
<SwitchPreference android:key="isNAT"
android:title="@string/nat"
android:summary="@string/nat_summary"/>
<Preference android:key="recovery"
android:title="@string/recovery"
android:summary="@string/recovery_summary"/>
</PreferenceScreen>
...@@ -7,44 +7,37 @@ ...@@ -7,44 +7,37 @@
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="profileName" android:key="profileName"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/profile_name"/> android:title="@string/profile_name"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="proxy" android:key="proxy"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/proxy"/> android:title="@string/proxy"/>
<be.mygod.preference.NumberPickerPreference <be.mygod.preference.NumberPickerPreference
app:min="1" app:min="1"
app:max="65535" app:max="65535"
android:key="remotePortNum" android:key="remotePortNum"
android:persistent="false"
android:summary="%d" android:summary="%d"
android:title="@string/remote_port"/> android:title="@string/remote_port"/>
<be.mygod.preference.NumberPickerPreference <be.mygod.preference.NumberPickerPreference
app:min="1025" app:min="1025"
app:max="65535" app:max="65535"
android:key="localPortNum" android:key="localPortNum"
android:persistent="false"
android:summary="%d" android:summary="%d"
android:title="@string/port"/> android:title="@string/port"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:inputType="textPassword" android:inputType="textPassword"
android:key="sitekey" android:key="sitekey"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/sitekey"/> android:title="@string/sitekey"/>
<DropDownPreference <DropDownPreference
android:key="encMethod" android:key="encMethod"
android:persistent="false"
android:entries="@array/enc_method_entry" android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value" android:entryValues="@array/enc_method_value"
android:summary="%s" android:summary="%s"
android:title="@string/enc_method"/> android:title="@string/enc_method"/>
<SwitchPreference <SwitchPreference
android:key="isAuth" android:key="isAuth"
android:persistent="false"
android:summary="@string/onetime_auth_summary" android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/> android:title="@string/onetime_auth"/>
...@@ -55,7 +48,6 @@ ...@@ -55,7 +48,6 @@
<DropDownPreference <DropDownPreference
android:key="route" android:key="route"
android:persistent="false"
android:entries="@array/route_entry" android:entries="@array/route_entry"
android:entryValues="@array/route_value" android:entryValues="@array/route_value"
android:summary="%s" android:summary="%s"
...@@ -63,20 +55,17 @@ ...@@ -63,20 +55,17 @@
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="remoteDns" android:key="remoteDns"
android:title="@string/remote_dns" android:title="@string/remote_dns"
android:summary="@string/remote_dns_summary"/> android:summary="%s"/>
<SwitchPreference <SwitchPreference
android:key="isIpv6" android:key="isIpv6"
android:persistent="false"
android:summary="@string/ipv6_summary" android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/> android:title="@string/ipv6"/>
<SwitchPreference <SwitchPreference
android:key="isProxyApps" android:key="isProxyApps"
android:persistent="false"
android:summary="@string/proxied_apps_summary" android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/> android:title="@string/proxied_apps"/>
<SwitchPreference <SwitchPreference
android:key="isUdpDns" android:key="isUdpDns"
android:persistent="false"
android:summary="@string/udp_dns_summary" android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/> android:title="@string/udp_dns"/>
...@@ -87,7 +76,6 @@ ...@@ -87,7 +76,6 @@
<SwitchPreference <SwitchPreference
android:key="kcp" android:key="kcp"
android:persistent="false"
android:summary="@string/kcp_summary" android:summary="@string/kcp_summary"
android:title="@string/kcp"/> android:title="@string/kcp"/>
...@@ -95,41 +83,14 @@ ...@@ -95,41 +83,14 @@
app:min="1" app:min="1"
app:max="65535" app:max="65535"
android:key="kcpPort" android:key="kcpPort"
android:persistent="false"
android:summary="@string/kcp_port_summary" android:summary="@string/kcp_port_summary"
android:title="@string/kcp_port"/> android:title="@string/kcp_port"/>
<be.mygod.preference.EditTextPreference <be.mygod.preference.EditTextPreference
android:key="kcpcli" android:key="kcpcli"
android:persistent="false"
android:summary="%s" android:summary="%s"
android:title="@string/kcpcli"/> android:title="@string/kcpcli"/>
</be.mygod.preference.PreferenceCategory> </be.mygod.preference.PreferenceCategory>
<be.mygod.preference.PreferenceCategory android:title="@string/misc_cat">
<SwitchPreference
android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
<SwitchPreference android:key="isNAT"
android:title="@string/nat"
android:summary="@string/nat_summary"/>
<Preference android:key="recovery" android:title="@string/recovery"
android:summary="@string/recovery_summary"/>
<Preference android:key="about" android:title="@string/about"/>
</be.mygod.preference.PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>
...@@ -98,10 +98,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -98,10 +98,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName) proxiedApps.add(item.packageName)
check.setChecked(true) check.setChecked(true)
} }
if (!appsLoading.get) { if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
profile.individual = proxiedApps.mkString("\n")
app.profileManager.updateProfile(profile)
}
} }
} }
...@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private var loadingView: View = _ private var loadingView: View = _
private val appsLoading = new AtomicBoolean private val appsLoading = new AtomicBoolean
private var handler: Handler = _ private var handler: Handler = _
private val profile = app.currentProfile.orNull
private def initProxiedApps(str: String = profile.individual) = proxiedApps = str.split('\n').to[mutable.HashSet] private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) =
proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() { override def onDestroy() {
instance = null instance = null
...@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all => case R.id.action_apply_all =>
app.profileManager.getAllProfiles match { app.profileManager.getAllProfiles match {
case Some(profiles) => case Some(profiles) =>
val proxiedAppString = profile.individual val proxiedAppString = app.settings.getString(Key.individual, null)
profiles.foreach(p => { profiles.foreach(p => {
p.individual = proxiedAppString p.individual = proxiedAppString
app.profileManager.updateProfile(p) app.profileManager.updateProfile(p)
...@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
} }
return true return true
case R.id.action_export => case R.id.action_export =>
val bypass = profile.bypass val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = profile.individual val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString) val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
...@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "") val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean) bypassSwitch.setChecked(enabled.toBoolean)
profile.individual = apps app.editor.putString(Key.individual, apps).apply()
app.profileManager.updateProfile(profile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE) appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE) loadingView.setVisibility(View.VISIBLE)
...@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
protected override def onCreate(savedInstanceState: Bundle) { protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (profile == null) finish()
handler = new Handler() handler = new Handler()
this.setContentView(R.layout.layout_apps) this.setContentView(R.layout.layout_apps)
...@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener { ...@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu) toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (!profile.proxyApps) { if (!app.settings.getBoolean(Key.proxyApps, false)) app.editor.putBoolean(Key.proxyApps, true).apply()
profile.proxyApps = true
app.profileManager.updateProfile(profile)
}
findViewById(R.id.onSwitch).asInstanceOf[Switch] findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => { .setOnCheckedChangeListener((_, checked) => {
profile.proxyApps = checked app.editor.putBoolean(Key.proxyApps, checked).apply()
app.profileManager.updateProfile(profile)
finish() finish()
}) })
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch] bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(profile.bypass) bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) => { bypassSwitch.setOnCheckedChangeListener((_, checked) => app.editor.putBoolean(Key.bypass, checked).apply())
profile.bypass = checked
app.profileManager.updateProfile(profile)
})
initProxiedApps() initProxiedApps()
loadingView = findViewById(R.id.loading) loadingView = findViewById(R.id.loading)
......
package com.github.shadowsocks
import android.app.ProgressDialog
import android.os.{Bundle, Handler, Message}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.{Key, TcpFastOpen, Utils}
class GlobalConfigFragment extends PreferenceFragment {
private var progressDialog: ProgressDialog = _
override def onCreatePreferences(bundle: Bundle, key: String) {
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(getActivity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
tfo.setChecked(TcpFastOpen.sendEnabled)
tfo.setOnPreferenceChangeListener((_, v) => {
val value = v.asInstanceOf[Boolean]
val result = TcpFastOpen.enabled(value)
if (result != null && result != "Success.")
Snackbar.make(getActivity.findViewById(android.R.id.content), result, Snackbar.LENGTH_LONG).show()
value == TcpFastOpen.sendEnabled
})
if (!TcpFastOpen.supported) {
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
findPreference("recovery").setOnPreferenceClickListener(_ => {
app.track("GlobalConfigFragment", "reset")
Utils.stopSsService(getActivity)
val h = showProgress(R.string.recovering)
Utils.ThrowableFuture {
app.copyAssets()
h.sendEmptyMessage(0)
}
true
})
}
private def showProgress(msg: Int): Handler = {
clearDialog()
progressDialog = ProgressDialog.show(getActivity, "", getString(msg), true, false)
new Handler {
override def handleMessage(msg: Message) {
clearDialog()
}
}
}
def clearDialog() {
if (progressDialog != null && progressDialog.isShowing) {
if (!getActivity.isDestroyed) progressDialog.dismiss()
progressDialog = null
}
}
}
package com.github.shadowsocks
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
class GlobalSettingsFragment extends ToolbarFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_global_settings, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.settings)
}
}
...@@ -22,69 +22,58 @@ package com.github.shadowsocks ...@@ -22,69 +22,58 @@ package com.github.shadowsocks
import java.lang.System.currentTimeMillis import java.lang.System.currentTimeMillis
import java.net.{HttpURLConnection, URL} import java.net.{HttpURLConnection, URL}
import java.util import java.util.Locale
import java.util.{GregorianCalendar, Locale}
import android.app.Activity
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog} import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._ import android.content._
import android.graphics.Typeface import android.net.{Uri, VpnService}
import android.net.VpnService import android.nfc.{NdefMessage, NfcAdapter}
import android.os._ import android.os.{Build, Bundle, Handler}
import android.support.design.widget.{FloatingActionButton, Snackbar} import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.{View, ViewGroup} import android.view.View
import android.widget._ import android.webkit.{WebView, WebViewClient}
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database._ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.CloseUtils._ import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView} import com.google.android.gms.ads.AdView
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.{PrimaryDrawerItem, SecondaryDrawerItem}
import com.mikepenz.materialdrawer.{Drawer, DrawerBuilder}
import scala.util.Random object MainActivity {
private final val TAG = "MainActivity"
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
if (!cache.containsKey(assetPath)) {
try {
cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath))
} catch {
case e: Exception =>
Log.e(TAG, "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
private final val TAG = "Typefaces"
private final val cache = new util.Hashtable[String, Typeface]
}
object Shadowsocks {
// Constants
private final val TAG = "Shadowsocks"
private final val REQUEST_CONNECT = 1 private final val REQUEST_CONNECT = 1
private final val DRAWER_PROFILES = 0L
// sticky drawer items have negative ids
private final val DRAWER_GLOBAL_SETTINGS = -1L
private final val DRAWER_ABOUT = -2L
} }
class Shadowsocks extends AppCompatActivity with ServiceBoundContext { class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
import Shadowsocks._ with OnSharedPreferenceChangeListener {
import MainActivity._
// Variables // Variables
var serviceStarted: Boolean = _ var serviceStarted: Boolean = _
var fab: FloatingActionButton = _ var fab: FloatingActionButton = _
var fabProgressCircle: FABProgressCircle = _ var fabProgressCircle: FABProgressCircle = _
var progressDialog: ProgressDialog = _
var state = State.STOPPED var state = State.STOPPED
var currentProfile = new Profile var currentProfile = new Profile
var drawer: Drawer = _
private lazy val profilesFragment = new ProfilesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
// Services // Services
private val callback = new IShadowsocksServiceCallback.Stub { private val callback = new IShadowsocksServiceCallback.Stub {
...@@ -96,8 +85,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -96,8 +85,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
fab.setImageResource(R.drawable.ic_start_busy) fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false) fab.setEnabled(false)
fabProgressCircle.show() fabProgressCircle.show()
preferences.setEnabled(false) //stat.setVisibility(View.GONE)
stat.setVisibility(View.GONE)
case State.CONNECTED => case State.CONNECTED =>
fab.setBackgroundTintList(greenTint) fab.setBackgroundTintList(greenTint)
if (state == State.CONNECTING) { if (state == State.CONNECTING) {
...@@ -107,7 +95,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -107,7 +95,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
} }
fab.setEnabled(true) fab.setEnabled(true)
changeSwitch(checked = true) changeSwitch(checked = true)
preferences.setEnabled(false)
stat.setVisibility(View.VISIBLE) stat.setVisibility(View.VISIBLE)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else { if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE) connectionTestText.setVisibility(View.VISIBLE)
...@@ -121,20 +108,17 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -121,20 +108,17 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
if (m != null) { if (m != null) {
val snackbar = Snackbar.make(findViewById(android.R.id.content), val snackbar = Snackbar.make(findViewById(android.R.id.content),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG) getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn, if (m == getString(R.string.nat_no_root)) addDisableNatToSnackbar(snackbar)
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
snackbar.show() snackbar.show()
Log.e(TAG, "Error to start VPN service: " + m) Log.e(TAG, "Error to start VPN service: " + m)
} }
preferences.setEnabled(true) //stat.setVisibility(View.GONE)
stat.setVisibility(View.GONE)
case State.STOPPING => case State.STOPPING =>
fab.setBackgroundTintList(greyTint) fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy) fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false) fab.setEnabled(false)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
preferences.setEnabled(false) //stat.setVisibility(View.GONE)
stat.setVisibility(View.GONE)
} }
state = s state = s
}) })
...@@ -159,11 +143,14 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -159,11 +143,14 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
updateState() updateState()
if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) { if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) {
val snackbar = Snackbar.make(findViewById(android.R.id.content), R.string.nat_deprecated, Snackbar.LENGTH_LONG) val snackbar = Snackbar.make(findViewById(android.R.id.content), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
snackbar.setAction(R.string.switch_to_vpn, (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener) addDisableNatToSnackbar(snackbar)
snackbar.show() snackbar.show()
} }
} }
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false)): View.OnClickListener)
override def onServiceDisconnected() { override def onServiceDisconnected() {
if (fab != null) fab.setEnabled(false) if (fab != null) fab.setEnabled(false)
} }
...@@ -184,10 +171,13 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -184,10 +171,13 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700) 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 lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
private var adView: AdView = _ private var adView: AdView = _
private lazy val preferences =
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
val handler = new Handler() val handler = new Handler()
private val connectedListener: BroadcastReceiver = (_, _) =>
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
connectionTestText.setText(getString(R.string.connection_test_pending))
}
private def changeSwitch(checked: Boolean) { private def changeSwitch(checked: Boolean) {
serviceStarted = checked serviceStarted = checked
...@@ -198,21 +188,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -198,21 +188,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
} }
} }
private def showProgress(msg: Int): Handler = {
clearDialog()
progressDialog = ProgressDialog.show(this, "", getString(msg), true, false)
new Handler {
override def handleMessage(msg: Message) {
clearDialog()
}
}
}
def cancelStart() {
clearDialog()
changeSwitch(checked = false)
}
def prepareStartService() { def prepareStartService() {
Utils.ThrowableFuture { Utils.ThrowableFuture {
if (app.isNatEnabled) serviceLoad() else { if (app.isNatEnabled) serviceLoad() else {
...@@ -227,27 +202,30 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -227,27 +202,30 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
} }
override def onCreate(savedInstanceState: Bundle) { override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main) setContentView(R.layout.layout_main)
// Initialize Toolbar drawer = new DrawerBuilder()
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] .withActivity(this)
toolbar.setTitle("shadowsocks") // non-translatable logo // TODO: .withHeader(R.drawable.some_header)
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo) .addDrawerItems(
val field = classOf[Toolbar].getDeclaredField("mTitleTextView") new PrimaryDrawerItem()
field.setAccessible(true) .withIdentifier(DRAWER_PROFILES)
val title = field.get(toolbar).asInstanceOf[TextView] .withName(R.string.profiles)
title.setFocusable(true) // TODO: withIcons
title.setGravity(0x10) )
title.getLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT .addStickyDrawerItems(
title.setOnClickListener(_ => startActivity(new Intent(this, classOf[ProfileManagerActivity]))) new SecondaryDrawerItem()
val typedArray = obtainStyledAttributes(Array(R.attr.selectableItemBackgroundBorderless)) .withIdentifier(DRAWER_GLOBAL_SETTINGS)
title.setBackgroundResource(typedArray.getResourceId(0, 0)) .withName(R.string.settings),
typedArray.recycle() new SecondaryDrawerItem()
val tf = Typefaces.get(this, "fonts/Iceland.ttf") .withIdentifier(DRAWER_ABOUT)
if (tf != null) title.setTypeface(tf) .withName(R.string.about)
title.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_arrow_drop_down, 0) .withSelectable(false)
)
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.build()
if (savedInstanceState == null) displayFragment(profilesFragment)
stat = findViewById(R.id.stat) stat = findViewById(R.id.stat)
connectionTestText = findViewById(R.id.connection_test).asInstanceOf[TextView] connectionTestText = findViewById(R.id.connection_test).asInstanceOf[TextView]
txText = findViewById(R.id.tx).asInstanceOf[TextView] txText = findViewById(R.id.tx).asInstanceOf[TextView]
...@@ -307,6 +285,81 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -307,6 +285,81 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
updateTraffic(0, 0, 0, 0) updateTraffic(0, 0, 0, 0)
handler.post(attachServiceCallback) handler.post(attachServiceCallback)
app.settings.registerOnSharedPreferenceChangeListener(this)
registerReceiver(connectedListener, new IntentFilter(Action.CONNECTED))
val intent = getIntent
if (intent != null) handleShareIntent(intent)
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
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) {
// TODO: show error msg
return
}
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, null)
.setMessage(profiles.mkString("\n"))
.create()
dialog.show()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => handler.post(() => {
detachService()
attachServiceCallback()
})
case _ =>
}
private def displayFragment(fragment: ToolbarFragment) {
getFragmentManager.beginTransaction().replace(R.id.content, fragment).commitAllowingStateLoss()
drawer.closeDrawer()
}
override def onItemClick(view: View, position: Int, drawerItem: IDrawerItem[_, _ <: ViewHolder]): Boolean = {
drawerItem.getIdentifier match {
case DRAWER_PROFILES => displayFragment(profilesFragment)
case DRAWER_GLOBAL_SETTINGS => displayFragment(globalSettingsFragment)
case DRAWER_ABOUT =>
app.track(TAG, "about")
val web = new WebView(this)
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))) catch {
case _: android.content.ActivityNotFoundException => // Ignore
}
true
}
})
new AlertDialog.Builder(this)
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
.setNegativeButton(getString(android.R.string.ok), null)
.setView(web)
.create()
.show()
}
true // unexpected cases will throw exception
} }
private def hideCircle() { private def hideCircle() {
...@@ -317,97 +370,44 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -317,97 +370,44 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
} }
} }
private def updateState(resetConnectionTest: Boolean = true) { private def updateState() {
if (bgService != null) { if (bgService != null) {
bgService.getState match { bgService.getState match {
case State.CONNECTING => case State.CONNECTING =>
fab.setBackgroundTintList(greyTint) fab.setBackgroundTintList(greyTint)
serviceStarted = false serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy) fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show() fabProgressCircle.show()
stat.setVisibility(View.GONE) //stat.setVisibility(View.GONE)
case State.CONNECTED => case State.CONNECTED =>
fab.setBackgroundTintList(greenTint) fab.setBackgroundTintList(greenTint)
serviceStarted = true serviceStarted = true
fab.setImageResource(R.drawable.ic_start_connected) fab.setImageResource(R.drawable.ic_start_connected)
preferences.setEnabled(false)
fabProgressCircle.postDelayed(hideCircle, 100) fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.VISIBLE) stat.setVisibility(View.VISIBLE)
if (resetConnectionTest || state != State.CONNECTED)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
connectionTestText.setText(getString(R.string.connection_test_pending))
}
case State.STOPPING => case State.STOPPING =>
fab.setBackgroundTintList(greyTint) fab.setBackgroundTintList(greyTint)
serviceStarted = false serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy) fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show() fabProgressCircle.show()
stat.setVisibility(View.GONE) //stat.setVisibility(View.GONE)
case _ => case _ =>
fab.setBackgroundTintList(greyTint) fab.setBackgroundTintList(greyTint)
serviceStarted = false serviceStarted = false
fab.setImageResource(R.drawable.ic_start_idle) fab.setImageResource(R.drawable.ic_start_idle)
preferences.setEnabled(true)
fabProgressCircle.postDelayed(hideCircle, 100) fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.GONE) //stat.setVisibility(View.GONE)
} }
state = bgService.getState state = bgService.getState
} }
} }
private def updateCurrentProfile() = {
// Check if current profile changed
if (preferences.profile == null || app.profileId != preferences.profile.id) {
updatePreferenceScreen(app.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
app.switchProfile((app.profileManager.getFirstProfile match {
case Some(first) => first
case None => app.profileManager.createDefault()
}).id)
})
if (serviceStarted) serviceLoad()
true
} else {
preferences.refreshProfile()
false
}
}
protected override def onResume() { protected override def onResume() {
super.onResume() super.onResume()
app.refreshContainerHolder app.refreshContainerHolder()
updateState(updateCurrentProfile())
}
private def updatePreferenceScreen(profile: Profile) {
if (profile.host == "198.199.101.152") if (adView == null) {
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1)
// Demographics
val random = new Random()
val adBuilder = new AdRequest.Builder()
adBuilder.setGender(AdRequest.GENDER_MALE)
val year = 1975 + random.nextInt(40)
val month = 1 + random.nextInt(12)
val day = random.nextInt(28)
adBuilder.setBirthday(new GregorianCalendar(year, month, day).getTime)
// Load Ad updateState()
adView.loadAd(adBuilder.build())
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
preferences.setProfile(profile)
} }
override def onStart() { override def onStart() {
...@@ -417,30 +417,22 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -417,30 +417,22 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
override def onStop() { override def onStop() {
super.onStop() super.onStop()
unregisterCallback() unregisterCallback()
clearDialog()
} }
override def onDestroy() { override def onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceiver(connectedListener)
app.settings.unregisterOnSharedPreferenceChangeListener(this)
detachService() detachService()
new BackupManager(this).dataChanged() new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
} }
def recovery() {
if (serviceStarted) serviceStop()
val h = showProgress(R.string.recovering)
Utils.ThrowableFuture {
app.copyAssets()
h.sendEmptyMessage(0)
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => case Activity.RESULT_OK =>
serviceLoad() serviceLoad()
case _ => case _ =>
cancelStart() changeSwitch(checked = false)
Log.e(TAG, "Failed to start VpnService") Log.e(TAG, "Failed to start VpnService")
} }
...@@ -456,11 +448,4 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { ...@@ -456,11 +448,4 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
changeSwitch(checked = false) changeSwitch(checked = false)
} }
} }
def clearDialog() {
if (progressDialog != null && progressDialog.isShowing) {
if (!isDestroyed) progressDialog.dismiss()
progressDialog = null
}
}
} }
package com.github.shadowsocks
import android.app.Activity
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
class ProfileConfigActivity extends Activity {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profile_config)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle("Profile config") // TODO
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => finish())
toolbar.inflateMenu(R.menu.profile_config_menu)
toolbar.setOnMenuItemClickListener(getFragmentManager.findFragmentById(R.id.content)
.asInstanceOf[OnMenuItemClickListener])
}
}
...@@ -18,68 +18,86 @@ ...@@ -18,68 +18,86 @@
/* */ /* */
/*******************************************************************************/ /*******************************************************************************/
package com.github.shadowsocks.widget package com.github.shadowsocks
import android.animation.ValueAnimator import android.content._
import android.content.Context import android.os._
import android.support.design.widget.CoordinatorLayout import android.support.v14.preference.SwitchPreference
import android.support.design.widget.CoordinatorLayout.Behavior import android.support.v4.content.LocalBroadcastManager
import android.support.design.widget.Snackbar.SnackbarLayout import android.support.v7.app.AlertDialog
import android.support.v4.view.animation.FastOutSlowInInterpolator import android.support.v7.preference.Preference
import android.util.AttributeSet import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.view.View import android.view.MenuItem
import com.github.clans.fab.FloatingActionMenu import be.mygod.preference._
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
import com.github.shadowsocks.utils.{Action, Key, Utils}
import scala.collection.JavaConverters._ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener {
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private var profile: Profile = _
private var isProxyApps: SwitchPreference = _
/** override def onCreatePreferences(bundle: Bundle, key: String) {
* Behavior for com.github.clans.fab.FloatingActionMenu that is aware of Snackbars and scrolling. app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
* case Some(p) =>
* @author Mygod profile = p
*/ profile.serialize(app.editor).apply()
class FloatingActionMenuBehavior(context: Context, attrs: AttributeSet) case None => getActivity.finish()
extends Behavior[FloatingActionMenu](context, attrs) {
private var fabTranslationYAnimator: ValueAnimator = _
private var fabTranslationY: Float = _
override def layoutDependsOn(parent: CoordinatorLayout, child: FloatingActionMenu, dependency: View): Boolean =
dependency.isInstanceOf[SnackbarLayout]
override def onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionMenu,
dependency: View): Boolean = {
dependency match {
case _: SnackbarLayout =>
var targetTransY = parent.getDependencies(child).asScala
.filter(view => view.isInstanceOf[SnackbarLayout] && parent.doViewsOverlap(child, view))
.map(view => view.getTranslationY - view.getHeight).reduceOption(_ min _).getOrElse(0F)
if (targetTransY > 0) targetTransY = 0
if (fabTranslationY != targetTransY) {
val currentTransY = child.getTranslationY
if (fabTranslationYAnimator != null && fabTranslationYAnimator.isRunning) fabTranslationYAnimator.cancel()
if (child.isShown && Math.abs(currentTransY - targetTransY) > child.getHeight * 0.667F) {
if (fabTranslationYAnimator == null) {
fabTranslationYAnimator = new ValueAnimator
fabTranslationYAnimator.setInterpolator(new FastOutSlowInInterpolator)
fabTranslationYAnimator.addUpdateListener(animation =>
child.setTranslationY(animation.getAnimatedValue.asInstanceOf[Float]))
}
fabTranslationYAnimator.setFloatValues(currentTransY, targetTransY)
fabTranslationYAnimator.start()
} else child.setTranslationY(targetTransY)
fabTranslationY = targetTransY
} }
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && getActivity.getSystemService(classOf[UserManager]).isDemoUser) {
findPreference(Key.host).setSummary("shadowsocks.example.org")
findPreference(Key.remotePort).setSummary("1337")
findPreference(Key.password).setSummary("\u2022" * 32)
} }
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setEnabled(Utils.isLollipopOrAbove || app.isNatEnabled)
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(getActivity, classOf[AppManager]))
isProxyApps.setChecked(true)
false false
})
}
override def onResume() {
super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = preference.getKey match {
case Key.kcpcli => displayPreferenceDialog(Key.kcpcli, new KcpCliPreferenceDialogFragment())
case _ => super.onDisplayPreferenceDialog(preference)
} }
override def onStartNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, directTargetChild: View, override def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
target: View, nestedScrollAxes: Int) = true case R.id.action_qr_code_nfc =>
override def onNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, target: View, dxConsumed: Int, getFragmentManager.beginTransaction().add(new QRCodeDialog(profile.toString), "").commitAllowingStateLoss()
dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) { true
super.onNestedScroll(parent, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) case R.id.action_export =>
val dy = dyConsumed + dyUnconsumed clipboard.setPrimaryClip(ClipData.newPlainText(null, profile.toString))
if (child.isMenuButtonHidden) { true
if (dy < 0) child.showMenuButton(true) case R.id.action_delete =>
} else if (dy > 0) child.hideMenuButton(true) new AlertDialog.Builder(getActivity)
.setTitle("Confirm?") // TODO
.setPositiveButton(android.R.string.yes, ((_, _) => {
app.profileManager.delProfile(profile.id)
LocalBroadcastManager.getInstance(getActivity)
.sendBroadcast(new Intent(Action.PROFILE_REMOVED).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
getActivity.finish()
}): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, null)
.create()
.show()
true
case R.id.action_apply =>
profile.deserialize(app.settings)
app.profileManager.updateProfile(profile)
LocalBroadcastManager.getInstance(getActivity)
.sendBroadcast(new Intent(Action.PROFILE_CHANGED).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
getActivity.finish()
true
case _ => false
} }
} }
...@@ -20,131 +20,78 @@ ...@@ -20,131 +20,78 @@
package com.github.shadowsocks package com.github.shadowsocks
import java.nio.charset.Charset import android.app.Activity
import android.app.TaskStackBuilder
import android.content._ import android.content._
import android.content.pm.PackageManager import android.graphics.Typeface
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.os.{Build, Bundle, Handler, UserManager} import android.os.{Build, Bundle, Handler, UserManager}
import android.provider.Settings import android.support.v4.content.LocalBroadcastManager
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget._ import android.support.v7.widget._
import android.support.v7.widget.helper.ItemTouchHelper import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.text.style.TextAppearanceSpan
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
import android.view._ import android.view._
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast} import android.widget.{TextView, Toast}
import com.github.clans.fab.{FloatingActionButton, FloatingActionMenu}
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils} import com.github.shadowsocks.utils.{Action, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext final class ProfilesFragment extends ToolbarFragment with OnMenuItemClickListener {
with View.OnClickListener with CreateNdefMessageCallback {
private final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) private final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnKeyListener { with View.OnClickListener {
var item: Profile = _ var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView] private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
itemView.setOnKeyListener(this)
{ {
val shareBtn = itemView.findViewById(R.id.share) val edit = itemView.findViewById(R.id.edit)
shareBtn.setOnClickListener(_ => { edit.setOnClickListener(_ => startActivity(new Intent(getActivity, classOf[ProfileConfigActivity])
val url = item.toString .putExtra(Action.EXTRA_PROFILE_ID, item.id)))
if (isNfcBeamEnabled) { edit.setOnLongClickListener(_ => {
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this) Utils.positionToast(Toast.makeText(getActivity, edit.getContentDescription, Toast.LENGTH_SHORT), edit,
nfcShareItem = url.getBytes(Charset.forName("UTF-8")) getActivity.getWindow, 0, Utils.dpToPx(getActivity, 8)).show()
}
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.close, null)
.setNegativeButton(R.string.copy_url, ((_, _) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
.setView(image)
.setTitle(R.string.share)
.create()
if (!isNfcAvailable) dialog.setMessage(getString(R.string.share_message_without_nfc))
else if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc),
((_, _) => startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.share_message))
dialog.setOnDismissListener(_ =>
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
}
dialog.show()
})
shareBtn.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.share, Toast.LENGTH_SHORT), shareBtn,
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show()
true true
}) })
} }
def updateText(txTotal: Long = 0, rxTotal: Long = 0) { def updateText(txTotal: Long = 0, rxTotal: Long = 0) {
val builder = new SpannableStringBuilder
val tx = item.tx + txTotal val tx = item.tx + txTotal
val rx = item.rx + rxTotal val rx = item.rx + rxTotal
builder.append(if (isDemoMode) "Profile #" + item.id else item.name) var title = if (isDemoMode) "Profile #" + item.id else item.name
val builder = new StringBuilder()
val address = (if (item.host.contains(":")) "[%s]:%d" else "%s:%d").format(item.host, item.remotePort)
if (title == null || title.isEmpty) title = address else builder.append(address)
if (tx != 0 || rx != 0) { if (tx != 0 || rx != 0) {
val start = builder.length if (builder.nonEmpty) builder.append('\n')
builder.append(getString(R.string.stat_profiles, builder.append(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx))) TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
builder.setSpan(new TextAppearanceSpan(ProfileManagerActivity.this, android.R.style.TextAppearance_Small),
start + 1, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
handler.post(() => text.setText(builder)) handler.post(() => {
text1.setText(title)
text2.setText(builder)
text2.setVisibility(if (builder.isEmpty) View.GONE else View.VISIBLE)
})
} }
def bind(item: Profile) { def bind(item: Profile) {
this.item = item this.item = item
updateText() updateText()
if (item.id == app.profileId) { if (item.id == app.profileId) {
text.setChecked(true) text1.setTypeface(null, Typeface.BOLD)
selectedItem = this selectedItem = this
} else { } else {
text.setChecked(false) text1.setTypeface(null, Typeface.NORMAL)
if (selectedItem eq this) selectedItem = null if (selectedItem eq this) selectedItem = null
} }
} }
def onClick(v: View) { def onClick(v: View): Unit = app.switchProfile(item.id)
app.switchProfile(item.id)
finish()
}
def onKey(v: View, keyCode: Int, event: KeyEvent): Boolean =
if (event.getAction == KeyEvent.ACTION_DOWN) keyCode match {
case KeyEvent.KEYCODE_DPAD_LEFT =>
val index = getAdapterPosition
if (index >= 0) {
profilesAdapter.remove(index)
undoManager.remove(index, item)
true
} else false
case _ => false
} else false
} }
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] { private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
...@@ -196,46 +143,48 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic ...@@ -196,46 +143,48 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
app.profileManager.delProfile(item.id) app.profileManager.delProfile(item.id)
if (item.id == app.profileId) app.profileId(-1) if (item.id == app.profileId) app.profileId(-1)
} }
def refreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
profiles(index) = app.profileManager.getProfile(id).get
notifyItemChanged(index)
}
def removeId(id: Int) {
val index = profiles.indexWhere(_.id == id)
profiles.remove(index)
notifyItemRemoved(index)
}
} }
private var selectedItem: ProfileViewHolder = _ private var selectedItem: ProfileViewHolder = _
private val handler = new Handler private val handler = new Handler
private val profilesListener: BroadcastReceiver = (_, intent) => {
private var menu : FloatingActionMenu = _ val id = intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)
intent.getAction match {
case Action.PROFILE_CHANGED => profilesAdapter.refreshId(id)
case Action.PROFILE_REMOVED => profilesAdapter.removeId(id)
}
}
private lazy val profilesAdapter = new ProfilesAdapter private lazy val profilesAdapter = new ProfilesAdapter
private var undoManager: UndoSnackbarManager[Profile] = _ private var undoManager: UndoSnackbarManager[Profile] = _
private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager] private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private lazy val isDemoMode = Build.VERSION.SDK_INT >= 25 && getSystemService(classOf[UserManager]).isDemoUser private lazy val isDemoMode = Build.VERSION.SDK_INT >= 25 &&
getActivity.getSystemService(classOf[UserManager]).isDemoUser
private var nfcAdapter : NfcAdapter = _
private var nfcShareItem: Array[Byte] = _
private var isNfcAvailable: Boolean = _
private var isNfcEnabled: Boolean = _
private var isNfcBeamEnabled: Boolean = _
override def onCreate(savedInstanceState: Bundle) { override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
super.onCreate(savedInstanceState) inflater.inflate(R.layout.layout_profiles, container, false)
setContentView(R.layout.layout_profiles)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar] override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.profiles) toolbar.setTitle(R.string.profiles)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
})
toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
initFab()
app.profileManager.setProfileAddedListener(profilesAdapter.add) app.profileManager.setProfileAddedListener(profilesAdapter.add)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView] val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(this) val layoutManager = new LinearLayoutManager(getActivity)
profilesList.setLayoutManager(layoutManager) profilesList.setLayoutManager(layoutManager)
profilesList.setItemAnimator(new DefaultItemAnimator) profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter) profilesList.setAdapter(profilesAdapter)
...@@ -255,169 +204,65 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic ...@@ -255,169 +204,65 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
true true
} }
}).attachToRecyclerView(profilesList) }).attachToRecyclerView(profilesList)
val filter = new IntentFilter(Action.PROFILE_CHANGED)
filter.addAction(Action.PROFILE_REMOVED)
LocalBroadcastManager.getInstance(getActivity).registerReceiver(profilesListener, filter)
}
attachService(new IShadowsocksServiceCallback.Stub { override def onAttach(activity: Activity) {
super.onAttach(activity)
activity.asInstanceOf[ServiceBoundContext].attachService(new IShadowsocksServiceCallback.Stub {
def stateChanged(state: Int, profileName: String, msg: String): Unit = () // ignore def stateChanged(state: Int, profileName: String, msg: String): Unit = () // ignore
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal) if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
}) })
if (app.settings.getBoolean(Key.profileTip, true)) {
app.editor.putBoolean(Key.profileTip, false).apply()
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create().show()
} }
val intent = getIntent override def onStart() {
if (intent != null) handleShareIntent(intent) super.onStart()
getActivity.asInstanceOf[ServiceBoundContext].registerCallback()
} }
def initFab() { override def onStop() {
menu = findViewById(R.id.menu).asInstanceOf[FloatingActionMenu] super.onStop()
menu.setClosedOnTouchOutside(true) getActivity.asInstanceOf[ServiceBoundContext].unregisterCallback()
val dm = AppCompatDrawableManager.get
val manualAddFAB = findViewById(R.id.fab_manual_add).asInstanceOf[FloatingActionButton]
manualAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_create))
manualAddFAB.setOnClickListener(this)
val qrcodeAddFAB = findViewById(R.id.fab_qrcode_add).asInstanceOf[FloatingActionButton]
qrcodeAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_image_camera_alt))
qrcodeAddFAB.setOnClickListener(this)
val nfcAddFAB = findViewById(R.id.fab_nfc_add).asInstanceOf[FloatingActionButton]
nfcAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_device_nfc))
nfcAddFAB.setOnClickListener(this)
val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste))
importAddFAB.setOnClickListener(this)
menu.setOnMenuToggleListener(opened => if (opened) qrcodeAddFAB.setVisibility(
if (getPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) View.VISIBLE else View.GONE))
} }
override def onDetach() {
override def onResume() { getActivity.asInstanceOf[ServiceBoundContext].detachService()
super.onResume() undoManager.flush()
updateNfcState() super.onDetach()
} }
override def onNewIntent(intent: Intent) { override def onDestroy() {
super.onNewIntent(intent) LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(profilesListener)
handleShareIntent(intent) super.onDestroy()
} }
override def onClick(v: View){ def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
v.getId match { case R.id.action_scan_qr_code =>
case R.id.fab_manual_add => startActivity(new Intent(getActivity, classOf[ScannerActivity]))
menu.toggle(true) true
val profile = app.profileManager.createProfile() case R.id.action_import =>
app.profileManager.updateProfile(profile)
app.switchProfile(profile.id)
finish()
case R.id.fab_qrcode_add =>
menu.toggle(false)
val intent = new Intent(this, classOf[ScannerActivity])
startActivity(intent)
case R.id.fab_nfc_add =>
menu.toggle(true)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.gotcha, null)
.setTitle(R.string.add_profile_nfc_hint_title)
.create()
if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.add_profile_nfc_hint))
}
dialog.show()
case R.id.fab_import_add =>
menu.toggle(true)
if (clipboard.hasPrimaryClip) { if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText) val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) { if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile) profiles.foreach(app.profileManager.createProfile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() Toast.makeText(getActivity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return return true
}
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
def updateNfcState() {
isNfcAvailable = false
isNfcEnabled = false
isNfcBeamEnabled = false
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter != null) {
isNfcAvailable = true
if (nfcAdapter.isEnabled) {
isNfcEnabled = true
if (nfcAdapter.isNdefPushEnabled) {
isNfcBeamEnabled = true
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
}
}
}
}
def handleShareIntent(intent: Intent) {
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
finish()
return
}
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
.setMessage(profiles.mkString("\n"))
.create()
dialog.show()
}
override def onStart() {
super.onStart()
registerCallback
}
override def onStop() {
super.onStop()
unregisterCallback
} }
override def onDestroy() {
detachService()
undoManager.flush()
app.profileManager.setProfileAddedListener(null)
super.onDestroy()
} }
Toast.makeText(getActivity, R.string.action_import_err, Toast.LENGTH_SHORT).show()
override def onBackPressed() { true
if (menu.isOpened) menu.close(true) else super.onBackPressed() case R.id.action_manual_settings =>
} val profile = app.profileManager.createProfile()
app.profileManager.updateProfile(profile)
def createNdefMessage(nfcEvent: NfcEvent) =
new NdefMessage(Array(new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem)))
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_export => case R.id.action_export =>
app.profileManager.getAllProfiles match { app.profileManager.getAllProfiles match {
case Some(profiles) => case Some(profiles) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n"))) clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() Toast.makeText(getActivity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show() case _ => Toast.makeText(getActivity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
} }
true true
case _ => false case _ => false
......
package com.github.shadowsocks
import java.nio.charset.Charset
import android.app.Activity
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter}
import android.os.Bundle
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ImageView, LinearLayout}
import com.github.shadowsocks.utils.Utils
import net.glxn.qrgen.android.QRCode
final class QRCodeDialog(url: String) extends DialogFragment {
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
private var adapter: NfcAdapter = _
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View = {
val image = new ImageView(getActivity)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(getActivity, 250), Utils.dpToPx(getActivity, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
image
}
override def onAttach(activity: Activity) {
superOnAttach(activity)
adapter = NfcAdapter.getDefaultAdapter(getActivity)
if (adapter != null) adapter.setNdefPushMessage(new NdefMessage(Array(
new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem))), activity)
}
override def onDetach() {
if (adapter != null) {
adapter.setNdefPushMessage(null, getActivity)
adapter = null
}
super.onDetach()
}
}
...@@ -46,14 +46,14 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient { ...@@ -46,14 +46,14 @@ trait ServiceBoundContext extends Context with IBinder.DeathRecipient {
} }
} }
protected def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try { def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try {
bgService.registerCallback(callback) bgService.registerCallback(callback)
callbackRegistered = true callbackRegistered = true
} catch { } catch {
case _: RemoteException => // Nothing case _: RemoteException => // Nothing
} }
protected def unregisterCallback() { def unregisterCallback() {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch { if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case _: RemoteException => case _: RemoteException =>
} }
......
...@@ -57,7 +57,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str ...@@ -57,7 +57,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setColor(ContextCompat.getColor(service, R.color.material_primary_500)) .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success)) .setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName) .setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks]) .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity])
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)) .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_stat_shadowsocks) .setSmallIcon(R.drawable.ic_stat_shadowsocks)
builder.addAction(R.drawable.ic_navigation_close, builder.addAction(R.drawable.ic_navigation_close,
......
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import java.util.Locale
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.{Intent, SharedPreferences}
import android.net.Uri
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.{DropDownPreference, Preference}
import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen, Utils}
import be.mygod.preference._
object ShadowsocksSettings {
// Constants
private final val TAG = "ShadowsocksSettings"
private val PROXY_PREFS = Array(Key.name, Key.host, Key.remotePort, Key.localPort, Key.password, Key.method,
Key.auth, Key.kcp, Key.kcpPort, Key.kcpcli)
private val FEATURE_PREFS = Array(Key.route, Key.remoteDns, Key.proxyApps, Key.udpdns, Key.ipv6)
// Helper functions
def updateDropDownPreference(pref: Preference, value: String) {
pref.asInstanceOf[DropDownPreference].setValue(value)
}
def updateEditTextPreference(pref: Preference, value: String) {
pref.asInstanceOf[EditTextPreference].setText(value)
}
def updateNumberPickerPreference(pref: Preference, value: Int) {
pref.asInstanceOf[NumberPickerPreference].setValue(value)
}
def updateSwitchPreference(pref: Preference, value: Boolean) {
pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile, demo: Boolean = false) {
name match {
case Key.name =>
updateEditTextPreference(pref, profile.name)
pref.setSummary(if (demo) "Profile #" + profile.id else "%s")
case Key.host =>
updateEditTextPreference(pref, profile.host)
pref.setSummary(if (demo) "shadowsocks.example.org" else "%s")
case Key.remotePort =>
updateNumberPickerPreference(pref, profile.remotePort)
pref.setSummary(if (demo) "1337" else "%d")
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
case Key.password =>
updateEditTextPreference(pref, profile.password)
pref.setSummary(if (demo) "\u2022" * 32 else "%s")
case Key.remoteDns =>
updateEditTextPreference(pref, profile.remoteDns)
if (profile.remoteDns != null)
pref.setSummary(if (demo) "8.8.8.8" else "%s")
case Key.method => updateDropDownPreference(pref, profile.method)
case Key.route => updateDropDownPreference(pref, profile.route)
case Key.proxyApps => updateSwitchPreference(pref, profile.proxyApps)
case Key.udpdns => updateSwitchPreference(pref, profile.udpdns)
case Key.auth => updateSwitchPreference(pref, profile.auth)
case Key.ipv6 => updateSwitchPreference(pref, profile.ipv6)
case Key.kcp => updateSwitchPreference(pref, profile.kcp)
case Key.kcpPort => updateNumberPickerPreference(pref, profile.kcpPort)
case Key.kcpcli => updateEditTextPreference(pref, profile.kcpcli)
}
}
}
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
import ShadowsocksSettings._
private def activity = getActivity.asInstanceOf[Shadowsocks]
lazy val natSwitch: SwitchPreference = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
private var isProxyApps: SwitchPreference = _
override def onCreatePreferences(bundle: Bundle, key: String) {
addPreferencesFromResource(R.xml.pref_all)
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
findPreference(Key.name).setOnPreferenceChangeListener((_, value) => {
profile.name = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.host).setOnPreferenceChangeListener((_, value) => {
profile.host = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.remotePort).setOnPreferenceChangeListener((_, value) => {
profile.remotePort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.localPort).setOnPreferenceChangeListener((_, value) => {
profile.localPort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.password).setOnPreferenceChangeListener((_, value) => {
profile.password = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.method).setOnPreferenceChangeListener((_, value) => {
profile.method = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.route).setOnPreferenceChangeListener((_, value) => {
profile.route = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.remoteDns).setOnPreferenceChangeListener((_, value) => {
profile.remoteDns = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcp).setOnPreferenceChangeListener((_, value) => {
profile.kcp = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcpPort).setOnPreferenceChangeListener((_, value) => {
profile.kcpPort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.kcpcli).setOnPreferenceChangeListener((_, value) => {
profile.kcpcli = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(activity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
isProxyApps.setOnPreferenceChangeListener((_, value) => {
profile.proxyApps = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.udpdns).setOnPreferenceChangeListener((_, value) => {
profile.udpdns = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.auth).setOnPreferenceChangeListener((_, value) => {
profile.auth = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.ipv6).setOnPreferenceChangeListener((_, value) => {
profile.ipv6 = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(activity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(activity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(activity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
tfo.setChecked(TcpFastOpen.sendEnabled)
tfo.setOnPreferenceChangeListener((_, v) => {
val value = v.asInstanceOf[Boolean]
val result = TcpFastOpen.enabled(value)
if (result != null && result != "Success.")
Snackbar.make(activity.findViewById(android.R.id.content), result, Snackbar.LENGTH_LONG).show()
value == TcpFastOpen.sendEnabled
})
if (!TcpFastOpen.supported) {
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
findPreference("recovery").setOnPreferenceClickListener(_ => {
app.track(TAG, "reset")
activity.recovery()
true
})
findPreference("about").setOnPreferenceClickListener(_ => {
app.track(TAG, "about")
val web = new WebView(activity)
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch {
case _: android.content.ActivityNotFoundException => // Ignore
}
true
}
})
new AlertDialog.Builder(activity)
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
.setNegativeButton(getString(android.R.string.ok), null)
.setView(web)
.create()
.show()
true
})
}
override def onDisplayPreferenceDialog(preference: Preference): Unit = preference.getKey match {
case Key.kcpcli => displayPreferenceDialog(Key.kcpcli, new KcpCliPreferenceDialogFragment())
case _ => super.onDisplayPreferenceDialog(preference)
}
def refreshProfile() {
profile = app.currentProfile match {
case Some(p) => p
case None =>
app.profileManager.getFirstProfile match {
case Some(p) =>
app.profileId(p.id)
p
case None =>
val default = app.profileManager.createDefault()
app.profileId(default.id)
default
}
}
isProxyApps.setChecked(profile.proxyApps)
}
override def onDestroy() {
super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this)
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT =>
activity.handler.post(() => {
activity.detachService()
activity.attachServiceCallback()
})
case _ =>
}
private var enabled = true
def setEnabled(enabled: Boolean) {
this.enabled = enabled
for (name <- Key.isNAT #:: PROXY_PREFS.toStream #::: FEATURE_PREFS.toStream) {
val pref = findPreference(name)
if (pref != null) pref.setEnabled(enabled &&
(name != Key.proxyApps || Utils.isLollipopOrAbove || app.isNatEnabled))
}
}
var profile: Profile = _
def setProfile(profile: Profile) {
this.profile = profile
val demo = Build.VERSION.SDK_INT >= 25 && activity.getSystemService(classOf[UserManager]).isDemoUser
for (name <- Array(PROXY_PREFS, FEATURE_PREFS).flatten) updatePreference(findPreference(name), name, profile, demo)
}
}
package com.github.shadowsocks
import android.app.Fragment
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.view.View
class ToolbarFragment extends Fragment {
var toolbar: Toolbar = _
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
val activity = getActivity.asInstanceOf[MainActivity]
activity.drawer.setToolbar(activity, toolbar, true)
}
}
...@@ -23,8 +23,10 @@ package com.github.shadowsocks.database ...@@ -23,8 +23,10 @@ package com.github.shadowsocks.database
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
import android.content.SharedPreferences
import android.os.Binder import android.os.Binder
import android.util.Base64 import android.util.Base64
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField} import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile { class Profile {
...@@ -32,7 +34,7 @@ class Profile { ...@@ -32,7 +34,7 @@ class Profile {
var id: Int = 0 var id: Int = 0
@DatabaseField @DatabaseField
var name: String = "Untitled" var name: String = ""
@DatabaseField @DatabaseField
var host: String = "" var host: String = ""
...@@ -100,4 +102,41 @@ class Profile { ...@@ -100,4 +102,41 @@ class Profile {
'#' + URLEncoder.encode(name, "utf-8") '#' + URLEncoder.encode(name, "utf-8")
def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method) def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method)
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
.putString(Key.host, host)
.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort)
.putString(Key.password, password)
.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns)
.putBoolean(Key.proxyApps, proxyApps)
.putBoolean(Key.bypass, bypass)
.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.auth, auth)
.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual)
.putBoolean(Key.kcp, kcp)
.putInt(Key.kcpPort, kcpPort)
.putString(Key.kcpcli, kcpcli)
def deserialize(pref: SharedPreferences) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = pref.getString(Key.name, null)
host = pref.getString(Key.host, null)
localPort = pref.getInt(Key.localPort, 0)
remotePort = pref.getInt(Key.remotePort, 0)
password = pref.getString(Key.password, null)
route = pref.getString(Key.route, null)
remoteDns = pref.getString(Key.remoteDns, null)
proxyApps = pref.getBoolean(Key.proxyApps, false)
bypass = pref.getBoolean(Key.bypass, false)
udpdns = pref.getBoolean(Key.udpdns, false)
auth = pref.getBoolean(Key.auth, false)
ipv6 = pref.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null)
kcp = pref.getBoolean(Key.kcp, false)
kcpPort = pref.getInt(Key.kcpPort, 0)
kcpcli = pref.getString(Key.kcpcli, null)
}
} }
...@@ -151,8 +151,6 @@ object Key { ...@@ -151,8 +151,6 @@ object Key {
val localPort = "localPortNum" val localPort = "localPortNum"
val remoteDns = "remoteDns" val remoteDns = "remoteDns"
val profileTip = "profileTip"
val kcp = "kcp" val kcp = "kcp"
val kcpPort = "kcpPort" val kcpPort = "kcpPort"
val kcpcli = "kcpcli" val kcpcli = "kcpcli"
...@@ -170,9 +168,14 @@ object State { ...@@ -170,9 +168,14 @@ object State {
} }
object Action { object Action {
val SERVICE = "com.github.shadowsocks.SERVICE" final val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE" final val CLOSE = "com.github.shadowsocks.CLOSE"
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH" final val CONNECTED = "com.github.shadowsocks.CONNECTED" // TODO
final val PROFILE_CHANGED = "com.github.shadowsocks.PROFILE_CHANGED"
final val PROFILE_REMOVED = "com.github.shadowsocks.PROFILE_REMOVED"
final val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
} }
object Route { object Route {
......
package com.github.shadowsocks.utils
import java.util
import android.content.Context
import android.graphics.Typeface
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
if (!cache.containsKey(assetPath)) {
try {
cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath))
} catch {
case e: Exception =>
Log.e("Typefaces", "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
private final val cache = new util.Hashtable[String, Typeface]
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment