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")
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.android.support" % "cardview-v7" % "25.1.0" ::
"com.android.support" % "design" % "25.1.0" ::
"com.android.support" % "gridlayout-v7" % "25.1.0" ::
"com.android.support" % "preference-v14" % "25.1.0" ::
"com.evernote" % "android-job" % "1.1.3" ::
"com.github.clans" % "fab" % "1.6.4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.github.kevinsawicki" % "http-request" % "6.0" ::
"com.google.android.gms" % "play-services-ads" % "10.0.1" ::
"com.google.android.gms" % "play-services-analytics" % "10.0.1" ::
"com.google.android.gms" % "play-services-gcm" % "10.0.1" ::
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "materialdrawer" % "5.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
......
......@@ -38,7 +38,7 @@
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
<activity
android:name=".Shadowsocks"
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
......@@ -49,28 +49,6 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ShadowsocksRunnerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask">
</activity>
<activity
android:name=".ProfileManagerActivity"
android:label="@string/profiles"
android:parentActivityName=".Shadowsocks"
android:excludeFromRecents="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.github.shadowsocks.ProfileManagerActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
......@@ -82,18 +60,34 @@
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="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
android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".ProfileManagerActivity"
android:parentActivityName=".MainActivity"
android:excludeFromRecents="true"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".Shadowsocks"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
......
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 @@
android:viewportHeight="24.0">
<path
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>
<?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,83 +3,87 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent"
tools:ignore="UnknownIdInLayout" android:clipChildren="false">
<LinearLayout android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_below="@+id/toolbar"
android:animateLayoutChanges="true">
<include layout="@layout/toolbar_light_dark" />
<LinearLayout android:id="@+id/stat" android:elevation="4dp" android:visibility="gone"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight" android:gravity="center_vertical"
android:paddingStart="16dip" android:paddingEnd="16dip" android:focusable="true"
android:paddingTop="16dip" android:paddingBottom="16dip"
android:background="@drawable/background_stat" android:orientation="vertical"
android:nextFocusRight="@+id/fab">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:text="@string/stat"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/connection_test"
android:gravity="end"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<android.support.v7.widget.GridLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="72dip"
app:columnCount="4">
<TextView app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:ellipsize="marquee" android:text="@string/sent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"/>
<TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill_horizontal"/>
<TextView app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee" android:text="▲"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
<TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"
android:ellipsize="marquee" android:text="@string/received"/>
<TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" />
<TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/>
<TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
</android.support.v7.widget.GridLayout>
<FrameLayout android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/stat"/>
<LinearLayout android:id="@+id/stat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:paddingStart="16dip"
android:paddingEnd="16dip"
android:paddingTop="16dip"
android:paddingBottom="16dip"
android:focusable="true"
android:background="@drawable/background_stat"
android:orientation="vertical"
android:nextFocusRight="@+id/fab"
android:layout_alignParentBottom="true">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:text="@string/stat"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/connection_test"
android:gravity="end"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<android.support.v7.widget.GridLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="72dip"
app:columnCount="4">
<TextView app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:ellipsize="marquee" android:text="@string/sent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"/>
<TextView app:layout_column="1" android:id="@+id/tx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill_horizontal"/>
<TextView app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee" android:text="▲"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<TextView app:layout_column="3" android:id="@+id/txRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
<TextView app:layout_row="1" app:layout_column="0" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="end"
android:ellipsize="marquee" android:text="@string/received"/>
<TextView app:layout_row="1" app:layout_column="1" android:id="@+id/rx" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" />
<TextView app:layout_row="1" app:layout_column="2" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="▼"/>
<TextView app:layout_row="1" app:layout_column="3" android:id="@+id/rxRate" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:ellipsize="marquee"
android:layout_width="0dp" android:layout_height="wrap_content" app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal" android:gravity="end"/>
</android.support.v7.widget.GridLayout>
</LinearLayout>
<fragment android:name="com.github.shadowsocks.ShadowsocksSettings"
android:id="@android:id/content" android:layout_below="@+id/title_bar"
android:layout_width="match_parent" android:layout_height="match_parent"/>
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle" app:reusable="true"
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle"
app:reusable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:elevation="6dp"
android:layout_alignBottom="@+id/title_bar"
android:layout_height="wrap_content"
android:elevation="6dp"
android:layout_alignTop="@+id/stat"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/fab_margin_bottom">
android:layout_marginTop="@dimen/fab_margin_top">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/material_blue_grey_700"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<fragment android:id="@+id/content"
class="com.github.shadowsocks.ProfileConfigFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<android.support.design.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="0dp" android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom|end"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
app:menu_colorNormal="@color/material_primary_500"
app:menu_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:layout_behavior="com.github.shadowsocks.widget.FloatingActionMenuBehavior">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_manual_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_manual_settings"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_qrcode_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/add_profile_methods_scan_qr_code"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_nfc_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="NFC"
/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/fab_import_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="@color/material_primary_500"
app:fab_colorPressed="@color/material_primary_700"
app:fab_colorRipple="@android:color/white"
app:fab_size="mini"
app:fab_label="@string/action_import"
/>
</com.github.clans.fab.FloatingActionMenu>
</android.support.design.widget.CoordinatorLayout>
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="8dp"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/master/core/res/res/layout/select_dialog_singlechoice_material.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/container"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:focusable="true"
android:nextFocusRight="@+id/share">
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:gravity="center_vertical"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:maxLines="4"
android:scrollHorizontally="false" />
<ImageView android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_gravity="center_vertical"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share"
android:focusable="true"
android:nextFocusLeft="@+id/container"/>
</LinearLayout>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:nextFocusRight="@+id/edit"
android:layout_marginTop="8dp"
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_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_vertical"
android:maxLines="2"
android:ellipsize="end"/>
<ImageView android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Edit"
android:focusable="true"
android:nextFocusLeft="@+id/container"/>
</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"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_profile"
android:icon="@drawable/ic_content_add"
android:alphabeticShortcut="a"
android:numericShortcut="0"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_scan_qr_code"
android:title="@string/add_profile_methods_scan_qr_code"
android:alphabeticShortcut="s"
android:numericShortcut="1"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="2"/>
<item android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="3"/>
</menu>
</item>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:alphabeticShortcut="e"
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin_bottom">-28dp</dimen>
<dimen name="fab_margin_top">-28dp</dimen>
</resources>
......@@ -3,12 +3,13 @@
<resources>
<color name="material_green_700">#388E3C</color>
<color name="material_green_a700">#00C853</color>
<color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_700">@color/material_blue_grey_700</color>
<color name="material_accent_200">@color/material_blue_grey_500</color>
<color name="material_accent_200">@color/material_green_a700</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 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>
......@@ -6,11 +6,11 @@
<item name="android:textSize">24sp</item>
</style>
<style name="Theme.MaterialBase" parent="Theme.AppCompat.Light.NoActionBar">
<style name="Theme.MaterialBase" parent="MaterialDrawerTheme.Light.DarkToolbar">
<!--suppress NewApi -->
<item name="android:navigationBarColor">@color/material_primary_700</item>
<item name="colorAccent">@color/material_primary_500</item>
<item name="colorButtonNormal">@color/material_primary_500</item>
<item name="colorButtonNormal">@color/material_accent_200</item>
<item name="colorPrimary">@color/material_primary_500</item>
<item name="colorPrimaryDark">@color/material_primary_700</item>
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
......
<?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 @@
<be.mygod.preference.EditTextPreference
android:key="profileName"
android:persistent="false"
android:summary="%s"
android:title="@string/profile_name"/>
<be.mygod.preference.EditTextPreference
android:key="proxy"
android:persistent="false"
android:summary="%s"
android:title="@string/proxy"/>
<be.mygod.preference.NumberPickerPreference
app:min="1"
app:max="65535"
android:key="remotePortNum"
android:persistent="false"
android:summary="%d"
android:title="@string/remote_port"/>
<be.mygod.preference.NumberPickerPreference
app:min="1025"
app:max="65535"
android:key="localPortNum"
android:persistent="false"
android:summary="%d"
android:title="@string/port"/>
<be.mygod.preference.EditTextPreference
android:inputType="textPassword"
android:key="sitekey"
android:persistent="false"
android:summary="%s"
android:title="@string/sitekey"/>
<DropDownPreference
android:key="encMethod"
android:persistent="false"
android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
<SwitchPreference
android:key="isAuth"
android:persistent="false"
android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/>
......@@ -55,7 +48,6 @@
<DropDownPreference
android:key="route"
android:persistent="false"
android:entries="@array/route_entry"
android:entryValues="@array/route_value"
android:summary="%s"
......@@ -63,20 +55,17 @@
<be.mygod.preference.EditTextPreference
android:key="remoteDns"
android:title="@string/remote_dns"
android:summary="@string/remote_dns_summary"/>
android:summary="%s"/>
<SwitchPreference
android:key="isIpv6"
android:persistent="false"
android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/>
<SwitchPreference
android:key="isProxyApps"
android:persistent="false"
android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/>
<SwitchPreference
android:key="isUdpDns"
android:persistent="false"
android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/>
......@@ -87,7 +76,6 @@
<SwitchPreference
android:key="kcp"
android:persistent="false"
android:summary="@string/kcp_summary"
android:title="@string/kcp"/>
......@@ -95,41 +83,14 @@
app:min="1"
app:max="65535"
android:key="kcpPort"
android:persistent="false"
android:summary="@string/kcp_port_summary"
android:title="@string/kcp_port"/>
<be.mygod.preference.EditTextPreference
android:key="kcpcli"
android:persistent="false"
android:summary="%s"
android:title="@string/kcpcli"/>
</be.mygod.preference.PreferenceCategory>
<be.mygod.preference.PreferenceCategory android:title="@string/misc_cat">
<SwitchPreference
android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
<SwitchPreference android:key="isNAT"
android:title="@string/nat"
android:summary="@string/nat_summary"/>
<Preference android:key="recovery" android:title="@string/recovery"
android:summary="@string/recovery_summary"/>
<Preference android:key="about" android:title="@string/about"/>
</be.mygod.preference.PreferenceCategory>
</PreferenceScreen>
......@@ -98,10 +98,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName)
check.setChecked(true)
}
if (!appsLoading.get) {
profile.individual = proxiedApps.mkString("\n")
app.profileManager.updateProfile(profile)
}
if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
}
}
......@@ -124,9 +121,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private var loadingView: View = _
private val appsLoading = new AtomicBoolean
private var handler: Handler = _
private val profile = app.currentProfile.orNull
private def initProxiedApps(str: String = profile.individual) = proxiedApps = str.split('\n').to[mutable.HashSet]
private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) =
proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() {
instance = null
......@@ -143,7 +140,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = profile.individual
val proxiedAppString = app.settings.getString(Key.individual, null)
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
......@@ -153,8 +150,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
}
return true
case R.id.action_export =>
val bypass = profile.bypass
val proxiedAppString = profile.individual
val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
......@@ -170,8 +167,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
profile.individual = apps
app.profileManager.updateProfile(profile)
app.editor.putString(Key.individual, apps).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
......@@ -194,8 +190,6 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
protected override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
if (profile == null) finish()
handler = new Handler()
this.setContentView(R.layout.layout_apps)
......@@ -211,23 +205,16 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!profile.proxyApps) {
profile.proxyApps = true
app.profileManager.updateProfile(profile)
}
if (!app.settings.getBoolean(Key.proxyApps, false)) app.editor.putBoolean(Key.proxyApps, true).apply()
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
profile.proxyApps = checked
app.profileManager.updateProfile(profile)
app.editor.putBoolean(Key.proxyApps, checked).apply()
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(profile.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
profile.bypass = checked
app.profileManager.updateProfile(profile)
})
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) => app.editor.putBoolean(Key.bypass, checked).apply())
initProxiedApps()
loadingView = findViewById(R.id.loading)
......
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
import java.lang.System.currentTimeMillis
import java.net.{HttpURLConnection, URL}
import java.util
import java.util.{GregorianCalendar, Locale}
import java.util.Locale
import android.app.Activity
import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.graphics.Typeface
import android.net.VpnService
import android.os._
import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter}
import android.os.{Build, Bundle, Handler}
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log
import android.view.{View, ViewGroup}
import android.widget._
import android.view.View
import android.webkit.{WebView, WebViewClient}
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database._
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import scala.util.Random
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
if (!cache.containsKey(assetPath)) {
try {
cache.put(assetPath, Typeface.createFromAsset(c.getAssets, assetPath))
} catch {
case e: Exception =>
Log.e(TAG, "Could not get typeface '" + assetPath + "' because " + e.getMessage)
app.track(e)
return null
}
}
return cache.get(assetPath)
}
}
import com.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}
private final val TAG = "Typefaces"
private final val cache = new util.Hashtable[String, Typeface]
}
object Shadowsocks {
// Constants
private final val TAG = "Shadowsocks"
object MainActivity {
private final val TAG = "MainActivity"
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 {
import Shadowsocks._
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnSharedPreferenceChangeListener {
import MainActivity._
// Variables
var serviceStarted: Boolean = _
var fab: FloatingActionButton = _
var fabProgressCircle: FABProgressCircle = _
var progressDialog: ProgressDialog = _
var state = State.STOPPED
var currentProfile = new Profile
var drawer: Drawer = _
private lazy val profilesFragment = new ProfilesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
// Services
private val callback = new IShadowsocksServiceCallback.Stub {
......@@ -96,8 +85,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
fabProgressCircle.show()
preferences.setEnabled(false)
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
if (state == State.CONNECTING) {
......@@ -107,7 +95,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
}
fab.setEnabled(true)
changeSwitch(checked = true)
preferences.setEnabled(false)
stat.setVisibility(View.VISIBLE)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
......@@ -121,20 +108,17 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
if (m != null) {
val snackbar = Snackbar.make(findViewById(android.R.id.content),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn,
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
if (m == getString(R.string.nat_no_root)) addDisableNatToSnackbar(snackbar)
snackbar.show()
Log.e(TAG, "Error to start VPN service: " + m)
}
preferences.setEnabled(true)
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_start_busy)
fab.setEnabled(false)
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
preferences.setEnabled(false)
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
}
state = s
})
......@@ -159,11 +143,14 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
updateState()
if (Build.VERSION.SDK_INT >= 21 && app.isNatEnabled) {
val snackbar = Snackbar.make(findViewById(android.R.id.content), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
snackbar.setAction(R.string.switch_to_vpn, (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
addDisableNatToSnackbar(snackbar)
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() {
if (fab != null) fab.setEnabled(false)
}
......@@ -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 greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
private var adView: AdView = _
private lazy val preferences =
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
val handler = new Handler()
private 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) {
serviceStarted = checked
......@@ -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() {
Utils.ThrowableFuture {
if (app.isNatEnabled) serviceLoad() else {
......@@ -227,27 +202,30 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
}
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
// Initialize Toolbar
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle("shadowsocks") // non-translatable logo
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
val field = classOf[Toolbar].getDeclaredField("mTitleTextView")
field.setAccessible(true)
val title = field.get(toolbar).asInstanceOf[TextView]
title.setFocusable(true)
title.setGravity(0x10)
title.getLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
title.setOnClickListener(_ => startActivity(new Intent(this, classOf[ProfileManagerActivity])))
val typedArray = obtainStyledAttributes(Array(R.attr.selectableItemBackgroundBorderless))
title.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
title.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_arrow_drop_down, 0)
drawer = new DrawerBuilder()
.withActivity(this)
// TODO: .withHeader(R.drawable.some_header)
.addDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_PROFILES)
.withName(R.string.profiles)
// TODO: withIcons
)
.addStickyDrawerItems(
new SecondaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings),
new SecondaryDrawerItem()
.withIdentifier(DRAWER_ABOUT)
.withName(R.string.about)
.withSelectable(false)
)
.withOnDrawerItemClickListener(this)
.withActionBarDrawerToggle(true)
.build()
if (savedInstanceState == null) displayFragment(profilesFragment)
stat = findViewById(R.id.stat)
connectionTestText = findViewById(R.id.connection_test).asInstanceOf[TextView]
txText = findViewById(R.id.tx).asInstanceOf[TextView]
......@@ -297,8 +275,8 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (serviceStarted) serviceStop()
else if (bgService != null) prepareStartService()
else changeSwitch(checked = false))
else if (bgService != null) prepareStartService()
else changeSwitch(checked = false))
fab.setOnLongClickListener((v: View) => {
Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show()
......@@ -307,6 +285,81 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
updateTraffic(0, 0, 0, 0)
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() {
......@@ -317,97 +370,44 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
}
}
private def updateState(resetConnectionTest: Boolean = true) {
private def updateState() {
if (bgService != null) {
bgService.getState match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show()
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint)
serviceStarted = true
fab.setImageResource(R.drawable.ic_start_connected)
preferences.setEnabled(false)
fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.VISIBLE)
if (resetConnectionTest || state != State.CONNECTED)
if (app.isNatEnabled) connectionTestText.setVisibility(View.GONE) else {
connectionTestText.setVisibility(View.VISIBLE)
connectionTestText.setText(getString(R.string.connection_test_pending))
}
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_busy)
preferences.setEnabled(false)
fabProgressCircle.show()
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
case _ =>
fab.setBackgroundTintList(greyTint)
serviceStarted = false
fab.setImageResource(R.drawable.ic_start_idle)
preferences.setEnabled(true)
fabProgressCircle.postDelayed(hideCircle, 100)
stat.setVisibility(View.GONE)
//stat.setVisibility(View.GONE)
}
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() {
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
adView.loadAd(adBuilder.build())
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
preferences.setProfile(profile)
updateState()
}
override def onStart() {
......@@ -417,30 +417,22 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
override def onStop() {
super.onStop()
unregisterCallback()
clearDialog()
}
override def onDestroy() {
super.onDestroy()
unregisterReceiver(connectedListener)
app.settings.unregisterOnSharedPreferenceChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
}
def recovery() {
if (serviceStarted) serviceStop()
val h = showProgress(R.string.recovering)
Utils.ThrowableFuture {
app.copyAssets()
h.sendEmptyMessage(0)
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK =>
serviceLoad()
case _ =>
cancelStart()
changeSwitch(checked = false)
Log.e(TAG, "Failed to start VpnService")
}
......@@ -456,11 +448,4 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
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 @@
/* */
/*******************************************************************************/
package com.github.shadowsocks.widget
package com.github.shadowsocks
import android.animation.ValueAnimator
import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.CoordinatorLayout.Behavior
import android.support.design.widget.Snackbar.SnackbarLayout
import android.support.v4.view.animation.FastOutSlowInInterpolator
import android.util.AttributeSet
import android.view.View
import com.github.clans.fab.FloatingActionMenu
import android.content._
import android.os._
import android.support.v14.preference.SwitchPreference
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.app.AlertDialog
import android.support.v7.preference.Preference
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.view.MenuItem
import be.mygod.preference._
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 = _
/**
* Behavior for com.github.clans.fab.FloatingActionMenu that is aware of Snackbars and scrolling.
*
* @author Mygod
*/
class FloatingActionMenuBehavior(context: Context, attrs: AttributeSet)
extends Behavior[FloatingActionMenu](context, attrs) {
private var fabTranslationYAnimator: ValueAnimator = _
private var fabTranslationY: Float = _
override def onCreatePreferences(bundle: Bundle, key: String) {
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) =>
profile = p
profile.serialize(app.editor).apply()
case None => getActivity.finish()
}
addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && getActivity.getSystemService(classOf[UserManager]).isDemoUser) {
findPreference(Key.host).setSummary("shadowsocks.example.org")
findPreference(Key.remotePort).setSummary("1337")
findPreference(Key.password).setSummary("\u2022" * 32)
}
isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setEnabled(Utils.isLollipopOrAbove || app.isNatEnabled)
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(getActivity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
}
override def layoutDependsOn(parent: CoordinatorLayout, child: FloatingActionMenu, dependency: View): Boolean =
dependency.isInstanceOf[SnackbarLayout]
override def onResume() {
super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
}
override def onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionMenu,
dependency: View): Boolean = {
dependency match {
case _: SnackbarLayout =>
var targetTransY = parent.getDependencies(child).asScala
.filter(view => view.isInstanceOf[SnackbarLayout] && parent.doViewsOverlap(child, view))
.map(view => view.getTranslationY - view.getHeight).reduceOption(_ min _).getOrElse(0F)
if (targetTransY > 0) targetTransY = 0
if (fabTranslationY != targetTransY) {
val currentTransY = child.getTranslationY
if (fabTranslationYAnimator != null && fabTranslationYAnimator.isRunning) fabTranslationYAnimator.cancel()
if (child.isShown && Math.abs(currentTransY - targetTransY) > child.getHeight * 0.667F) {
if (fabTranslationYAnimator == null) {
fabTranslationYAnimator = new ValueAnimator
fabTranslationYAnimator.setInterpolator(new FastOutSlowInInterpolator)
fabTranslationYAnimator.addUpdateListener(animation =>
child.setTranslationY(animation.getAnimatedValue.asInstanceOf[Float]))
}
fabTranslationYAnimator.setFloatValues(currentTransY, targetTransY)
fabTranslationYAnimator.start()
} else child.setTranslationY(targetTransY)
fabTranslationY = targetTransY
}
}
false
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,
target: View, nestedScrollAxes: Int) = true
override def onNestedScroll(parent: CoordinatorLayout, child: FloatingActionMenu, target: View, dxConsumed: Int,
dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
super.onNestedScroll(parent, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
val dy = dyConsumed + dyUnconsumed
if (child.isMenuButtonHidden) {
if (dy < 0) child.showMenuButton(true)
} else if (dy > 0) child.hideMenuButton(true)
override def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_qr_code_nfc =>
getFragmentManager.beginTransaction().add(new QRCodeDialog(profile.toString), "").commitAllowingStateLoss()
true
case R.id.action_export =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profile.toString))
true
case R.id.action_delete =>
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 @@
package com.github.shadowsocks
import java.nio.charset.Charset
import android.app.TaskStackBuilder
import android.app.Activity
import android.content._
import android.content.pm.PackageManager
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.graphics.Typeface
import android.os.{Build, Bundle, Handler, UserManager}
import android.provider.Settings
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget._
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.text.style.TextAppearanceSpan
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
import android.view._
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
import com.github.clans.fab.{FloatingActionButton, FloatingActionMenu}
import android.widget.{TextView, Toast}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.utils.{Action, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.widget.UndoSnackbarManager
import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.ArrayBuffer
final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
with View.OnClickListener with CreateNdefMessageCallback {
final class ProfilesFragment extends ToolbarFragment with OnMenuItemClickListener {
private final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnKeyListener {
with View.OnClickListener {
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.setOnKeyListener(this)
{
val shareBtn = itemView.findViewById(R.id.share)
shareBtn.setOnClickListener(_ => {
val url = item.toString
if (isNfcBeamEnabled) {
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this)
nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
}
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.close, null)
.setNegativeButton(R.string.copy_url, ((_, _) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
.setView(image)
.setTitle(R.string.share)
.create()
if (!isNfcAvailable) dialog.setMessage(getString(R.string.share_message_without_nfc))
else if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc),
((_, _) => startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.share_message))
dialog.setOnDismissListener(_ =>
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
}
dialog.show()
})
shareBtn.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.share, Toast.LENGTH_SHORT), shareBtn,
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show()
val edit = itemView.findViewById(R.id.edit)
edit.setOnClickListener(_ => startActivity(new Intent(getActivity, classOf[ProfileConfigActivity])
.putExtra(Action.EXTRA_PROFILE_ID, item.id)))
edit.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(getActivity, edit.getContentDescription, Toast.LENGTH_SHORT), edit,
getActivity.getWindow, 0, Utils.dpToPx(getActivity, 8)).show()
true
})
}
def updateText(txTotal: Long = 0, rxTotal: Long = 0) {
val builder = new SpannableStringBuilder
val tx = item.tx + txTotal
val rx = item.rx + rxTotal
builder.append(if (isDemoMode) "Profile #" + item.id else item.name)
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) {
val start = builder.length
if (builder.nonEmpty) builder.append('\n')
builder.append(getString(R.string.stat_profiles,
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
builder.setSpan(new TextAppearanceSpan(ProfileManagerActivity.this, android.R.style.TextAppearance_Small),
start + 1, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
handler.post(() => text.setText(builder))
handler.post(() => {
text1.setText(title)
text2.setText(builder)
text2.setVisibility(if (builder.isEmpty) View.GONE else View.VISIBLE)
})
}
def bind(item: Profile) {
this.item = item
updateText()
if (item.id == app.profileId) {
text.setChecked(true)
text1.setTypeface(null, Typeface.BOLD)
selectedItem = this
} else {
text.setChecked(false)
text1.setTypeface(null, Typeface.NORMAL)
if (selectedItem eq this) selectedItem = null
}
}
def onClick(v: View) {
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
def onClick(v: View): Unit = app.switchProfile(item.id)
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
......@@ -196,46 +143,48 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
app.profileManager.delProfile(item.id)
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 val handler = new Handler
private var menu : FloatingActionMenu = _
private val profilesListener: BroadcastReceiver = (_, intent) => {
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 var undoManager: UndoSnackbarManager[Profile] = _
private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private lazy val isDemoMode = Build.VERSION.SDK_INT >= 25 && 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 = _
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private lazy val isDemoMode = Build.VERSION.SDK_INT >= 25 &&
getActivity.getSystemService(classOf[UserManager]).isDemoUser
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profiles)
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_profiles, container, false)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.profiles)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener(_ => {
val intent = getParentActivityIntent
if (shouldUpRecreateTask(intent) || isTaskRoot)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
else finish()
})
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
initFab()
app.profileManager.setProfileAddedListener(profilesAdapter.add)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(this)
val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(getActivity)
profilesList.setLayoutManager(layoutManager)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
......@@ -255,169 +204,65 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
true
}
}).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 trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
})
if (app.settings.getBoolean(Key.profileTip, true)) {
app.editor.putBoolean(Key.profileTip, false).apply()
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create().show()
}
val intent = getIntent
if (intent != null) handleShareIntent(intent)
}
def initFab() {
menu = findViewById(R.id.menu).asInstanceOf[FloatingActionMenu]
menu.setClosedOnTouchOutside(true)
val dm = AppCompatDrawableManager.get
val manualAddFAB = findViewById(R.id.fab_manual_add).asInstanceOf[FloatingActionButton]
manualAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_create))
manualAddFAB.setOnClickListener(this)
val qrcodeAddFAB = findViewById(R.id.fab_qrcode_add).asInstanceOf[FloatingActionButton]
qrcodeAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_image_camera_alt))
qrcodeAddFAB.setOnClickListener(this)
val nfcAddFAB = findViewById(R.id.fab_nfc_add).asInstanceOf[FloatingActionButton]
nfcAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_device_nfc))
nfcAddFAB.setOnClickListener(this)
val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste))
importAddFAB.setOnClickListener(this)
menu.setOnMenuToggleListener(opened => if (opened) qrcodeAddFAB.setVisibility(
if (getPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) View.VISIBLE else View.GONE))
}
override def onResume() {
super.onResume()
updateNfcState()
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
override def onClick(v: View){
v.getId match {
case R.id.fab_manual_add =>
menu.toggle(true)
val profile = app.profileManager.createProfile()
app.profileManager.updateProfile(profile)
app.switchProfile(profile.id)
finish()
case R.id.fab_qrcode_add =>
menu.toggle(false)
val intent = new Intent(this, classOf[ScannerActivity])
startActivity(intent)
case R.id.fab_nfc_add =>
menu.toggle(true)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setPositiveButton(R.string.gotcha, null)
.setTitle(R.string.add_profile_nfc_hint_title)
.create()
if (!isNfcBeamEnabled) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.add_profile_nfc_hint))
}
dialog.show()
case R.id.fab_import_add =>
menu.toggle(true)
if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return
}
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
def updateNfcState() {
isNfcAvailable = false
isNfcEnabled = false
isNfcBeamEnabled = false
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter != null) {
isNfcAvailable = true
if (nfcAdapter.isEnabled) {
isNfcEnabled = true
if (nfcAdapter.isNdefPushEnabled) {
isNfcBeamEnabled = true
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
}
}
}
}
def handleShareIntent(intent: Intent) {
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
finish()
return
}
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(app.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
.setMessage(profiles.mkString("\n"))
.create()
dialog.show()
}
override def onStart() {
super.onStart()
registerCallback
getActivity.asInstanceOf[ServiceBoundContext].registerCallback()
}
override def onStop() {
super.onStop()
unregisterCallback
getActivity.asInstanceOf[ServiceBoundContext].unregisterCallback()
}
override def onDestroy() {
detachService()
override def onDetach() {
getActivity.asInstanceOf[ServiceBoundContext].detachService()
undoManager.flush()
app.profileManager.setProfileAddedListener(null)
super.onDestroy()
super.onDetach()
}
override def onBackPressed() {
if (menu.isOpened) menu.close(true) else super.onBackPressed()
override def onDestroy() {
LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(profilesListener)
super.onDestroy()
}
def createNdefMessage(nfcEvent: NfcEvent) =
new NdefMessage(Array(new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem)))
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code =>
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(app.profileManager.createProfile)
Toast.makeText(getActivity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
return true
}
}
Toast.makeText(getActivity, R.string.action_import_err, Toast.LENGTH_SHORT).show()
true
case R.id.action_manual_settings =>
val profile = app.profileManager.createProfile()
app.profileManager.updateProfile(profile)
case R.id.action_export =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
Toast.makeText(getActivity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
case _ => Toast.makeText(getActivity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
}
true
case _ => false
......
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 {
}
}
protected def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try {
def registerCallback(): Unit = if (bgService != null && callback != null && !callbackRegistered) try {
bgService.registerCallback(callback)
callbackRegistered = true
} catch {
case _: RemoteException => // Nothing
}
protected def unregisterCallback() {
def unregisterCallback() {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case _: RemoteException =>
}
......
......@@ -57,7 +57,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
.setContentTitle(profileName)
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks])
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[MainActivity])
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
builder.addAction(R.drawable.ic_navigation_close,
......
/*******************************************************************************/
/* */
/* 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
import java.net.URLEncoder
import java.util.Locale
import android.content.SharedPreferences
import android.os.Binder
import android.util.Base64
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile {
......@@ -32,7 +34,7 @@ class Profile {
var id: Int = 0
@DatabaseField
var name: String = "Untitled"
var name: String = ""
@DatabaseField
var host: String = ""
......@@ -100,4 +102,41 @@ class Profile {
'#' + URLEncoder.encode(name, "utf-8")
def isMethodUnsafe: Boolean = "table".equalsIgnoreCase(method) || "rc4".equalsIgnoreCase(method)
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
.putString(Key.host, host)
.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort)
.putString(Key.password, password)
.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns)
.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 {
val localPort = "localPortNum"
val remoteDns = "remoteDns"
val profileTip = "profileTip"
val kcp = "kcp"
val kcpPort = "kcpPort"
val kcpcli = "kcpcli"
......@@ -170,9 +168,14 @@ object State {
}
object Action {
val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE"
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE"
final val 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 {
......
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