Commit 1b1f4233 authored by Max Lv's avatar Max Lv

Merge pull request #414 from Mygod/master

Add more material design (3)
parents e079ef31 d71c8dce
......@@ -29,16 +29,12 @@ libraryDependencies ++= Seq(
"com.github.kevinsawicki" % "http-request" % "6.0",
"commons-net" % "commons-net" % "3.3",
"com.google.zxing" % "android-integration" % "3.2.1",
"com.joanzapata.android" % "android-iconify" % "1.0.11",
"net.glxn.qrgen" % "android" % "2.0",
"net.simonvt.menudrawer" % "menudrawer" % "3.0.6",
"com.google.android.gms" % "play-services-base" % "8.1.0",
"com.google.android.gms" % "play-services-ads" % "8.1.0",
"com.google.android.gms" % "play-services-analytics" % "8.1.0",
"com.android.support" % "support-v4" % "23.1.0",
"com.android.support" % "design" % "23.1.0",
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
"com.nostra13.universalimageloader" % "universal-image-loader" % "1.9.4",
"com.j256.ormlite" % "ormlite-core" % "4.48",
"com.j256.ormlite" % "ormlite-android" % "4.48"
)
......@@ -46,13 +42,9 @@ libraryDependencies ++= Seq(
proguardOptions in Android ++= Seq("-keep class android.support.v4.app.** { *; }",
"-keep interface android.support.v4.app.** { *; }",
"-keep class android.support.v7.widget.Toolbar { <init>(...); }",
"-keep class com.actionbarsherlock.** { *; }",
"-keep interface com.actionbarsherlock.** { *; }",
"-keep class org.jraf.android.backport.** { *; }",
"-keep class com.github.shadowsocks.** { *; }",
"-keep class * extends com.j256.ormlite.** { *; }",
"-keep class com.joanzapata.** { *; }",
"-keepattributes *Annotation*",
"-dontwarn com.google.android.gms.internal.zzhu",
"-dontwarn org.xbill.**",
"-dontwarn com.actionbarsherlock.**")
"-dontwarn org.xbill.**")
......@@ -22,7 +22,8 @@
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent"
android:label="@string/app_name">
android:label="@string/app_name"
android:theme="@style/Theme.Material">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
......@@ -36,7 +37,6 @@
<activity
android:name=".Shadowsocks"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
......@@ -62,11 +62,22 @@
android:launchMode="singleTask">
</activity>
<activity
android:name=".ProfileManagerActivity"
android:label="@string/app_name"
android:parentActivityName=".Shadowsocks"
android:exported="false">
<intent-filter>
<action android:name="com.github.shadowsocks.ProfileManagerActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".AppManager"
android:theme="@style/Theme.Material"
android:label="@string/app_name"
android:parentActivityName=".Shadowsocks">
android:parentActivityName=".Shadowsocks"
android:exported="false">
<intent-filter>
<action android:name="com.github.shadowsocks.AppManager"/>
<category android:name="android.intent.category.DEFAULT"/>
......
Subproject commit 095aa7490cb39d513180d1f756e6751107511f98
Subproject commit 890e880dc61ad4f2e9a340a78c797c4aabee6429
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="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"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ab_texture_tile_shadow"
android:tileMode="repeat" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2011 The Android Open Source Project
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/btn_cab_done_pressed_shadow" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/btn_cab_done_focused_shadow" />
<item android:state_enabled="true"
android:drawable="@drawable/btn_cab_done_default_shadow" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2011 The Android Open Source Project
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background"
android:drawable="@drawable/progress_bg_shadow" />
<item android:id="@android:id/secondaryProgress">
<scale android:scaleWidth="100%"
android:drawable="@drawable/progress_secondary_shadow" />
</item>
<item android:id="@android:id/progress">
<scale android:scaleWidth="100%"
android:drawable="@drawable/progress_primary_shadow" />
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2011 The Android Open Source Project
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime" >
<item android:state_pressed="false" android:state_focused="true" android:drawable="@drawable/list_focused_shadow" />
<item android:state_pressed="true" android:drawable="@drawable/list_pressed_shadow" />
<item android:drawable="@android:color/transparent" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2011 The Android Open Source Project
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
android:drawable="@drawable/spinner_ab_disabled_shadow" />
<item android:state_pressed="true"
android:drawable="@drawable/spinner_ab_pressed_shadow" />
<item android:state_pressed="false" android:state_focused="true"
android:drawable="@drawable/spinner_ab_focused_shadow" />
<item android:drawable="@drawable/spinner_ab_default_shadow" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!-- File created by the Android Action Bar Style Generator
Copyright (C) 2011 The Android Open Source Project
Copyright (C) 2012 readyState Software Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non focused states -->
<item android:state_focused="false" android:state_selected="false" android:state_pressed="false" android:drawable="@android:color/transparent" />
<item android:state_focused="false" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_selected_shadow" />
<!-- Focused states -->
<item android:state_focused="true" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/tab_unselected_focused_shadow" />
<item android:state_focused="true" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_selected_focused_shadow" />
<!-- Pressed -->
<!-- Non focused states -->
<item android:state_focused="false" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/tab_unselected_pressed_shadow" />
<item android:state_focused="false" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/tab_selected_pressed_shadow" />
<!-- Focused states -->
<item android:state_focused="true" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/tab_unselected_pressed_shadow" />
<item android:state_focused="true" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/tab_selected_pressed_shadow" />
</selector>
......@@ -32,8 +32,17 @@
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"/>
</LinearLayout>
<ListView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/applistview"/>
<FrameLayout android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ProgressBar android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/applistview"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ads="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent"
tools:ignore="UnknownIdInLayout" android:clipChildren="false">
<include layout="@layout/toolbar_light_dark" />
<com.google.android.gms.ads.AdView
android:id="@+id/adView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
ads:adSize="SMART_BANNER"
ads:adUnitId="ca-app-pub-9097031975646651/7760346322">
</com.google.android.gms.ads.AdView>
<fragment android:name="com.github.shadowsocks.ShadowsocksSettings"
android:id="@android:id/content"
android:layout_below="@+id/adView"
android:layout_width="match_parent" android:layout_height="match_parent"/>
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle" app:reusable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:elevation="6dp"
android:layout_alignBottom="@+id/toolbar"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="@dimen/fab_margin_bottom">
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/material_blue_grey_700"
android:src="@drawable/ic_cloud_off"
app:pressedTranslationZ="6dp" app:borderWidth="0dp" />
</com.github.jorgecastilloprz.FABProgressCircle>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:paddingLeft="20dp"
android:paddingRight="?attr/dialogPreferredPadding">
<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:drawableLeft="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:maxLines="4"
android:scrollHorizontally="false" />
<ImageView android:id="@+id/qrcode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/qrcode"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/MenuDrawer.Widget.Category" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/MenuDrawer.Widget.Title" />
\ No newline at end of file
......@@ -7,6 +7,5 @@
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:titleTextAppearance="@style/Toolbar.TitleText"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:id="@+id/toolbar" />
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_profile"
android:icon="@drawable/ic_content_add"
android:alphabeticShortcut="a"
android:numericShortcut="0"
app:showAsAction="always">
<menu>
<item android:id="@+id/scan_qr_code"
android:title="@string/add_profile_methods_scan_qr_code"
android:alphabeticShortcut="s"
android:numericShortcut="1"/>
<item android:id="@+id/manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="2"/>
</menu>
</item>
</menu>
......@@ -3,9 +3,6 @@
<resources>
<string name="app_name">影梭</string>
<string name="screen_name">shadowsocks</string>
<string name="function_cat">服务控制</string>
<string name="service_controller">代理开关</string>
<string name="service_summary">启动/关闭后台服务</string>
<string name="proxy_cat">服务器设置</string>
<string name="profile">配置文件</string>
<string name="profile_summary">配置文件名称</string>
......@@ -39,26 +36,18 @@
<string name="traffic_stat_summary">在通知栏上显示当前上下行速率</string>
<string name="forward_success">后台服务已开始运行。</string>
<string name="service_running">已连接到「%s」</string>
<string name="service_status" formatted="false">当前速率:%d KB/s</string>
<string name="forward_fail">后台服务启动失败。</string>
<string name="service_stopped">后台服务已停止。</string>
<string name="forward_stop">后台服务已停止。</string>
<string name="service_failed">无法连接远程服务器</string>
<string name="stop">停止服务</string>
<string name="stopping">正在关闭...</string>
<string name="stopping">正在关闭……</string>
<string name="vpn_error">后台服务启动失败:%s</string>
<string name="ok_iknow">确定</string>
<string name="close">关闭</string>
<string name="port_alert">本地端口号应大于1024</string>
<string name="port_empty">端口号不能为空</string>
<string name="proxy_empty">代理服务器地址不能为空</string>
<string name="crash_alert">检测到意外退出,状态已被重置。</string>
<string name="connect">连接</string>
<string name="initializing">正在初始化...</string>
<string name="recovering">重置中...</string>
<string name="loading">正在加载...</string>
<string name="yes"></string>
<string name="no"></string>
<string name="initializing">正在初始化……</string>
<string name="recovering">重置中……</string>
<string name="loading">正在加载……</string>
<string name="profiles">配置文件</string>
<string name="settings">设置选项</string>
<string name="remove_profile">删除此配置文件「%s」?</string>
......@@ -66,13 +55,10 @@
<string name="about">关于</string>
<string name="about_title">影梭(shadowsocks)%s</string>
<string name="flush_dnscache">刷新 DNS 缓存</string>
<string name="flushing">刷新中...</string>
<string name="flushing">刷新中……</string>
<string name="qrcode">二维码</string>
<string name="add_profile">添加配置文件</string>
<string name="enable_nat">启用 NAT 模式.</string>
<string name="disable_nat">禁用 NAT 模式.</string>
<string name="udp_dns">UDP 转发</string>
<string name="udp_dns_summary">由远程服务器转发 UDP 协议的数据包</string>
......@@ -84,10 +70,8 @@
<!-- profile -->
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string-array name="add_profile_methods">
<item>扫描二维码</item>
<item>手动设置</item>
</string-array>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string>
<string-array name="route_entry">
<item>全局</item>
......@@ -101,6 +85,8 @@
<string name="action_export_err">导出失败</string>
<string name="action_import_msg">导入成功</string>
<string name="action_import_err">导入失败</string>
<string name="error">错误</string>
<string name="removed">已删除</string>
<string name="undo">撤销</string>
</resources>
......@@ -2,17 +2,7 @@
<resources>
<color name="title_background">#ff5e5e5e</color>
<color name="dialog_title_background">#17170443</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="grey1">#646464</color>
<color name="grey2">#c8c8c8</color>
<color name="grey3">#323232</color>
<color name="md__transparent">#00000000</color>
<color name="md__defaultBackground">#FF555555</color>
<color name="material_green_600">#43A047</color>
<color name="material_green_700">#388E3C</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>
......
<resources>
<!-- ID used when defining the content layout in XML. -->
<item name="mdContent" type="id"/>
<!-- ID used when defining the menu layout in XML. -->
<item name="mdMenu" type="id"/>
<!-- The ID of the content container. -->
<item name="md__content" type="id"/>
<!-- The ID of the menu container. -->
<item name="md__menu" type="id"/>
<!-- The ID of the drawer. -->
<item name="md__drawer" type="id"/>
<!-- Used with View#setTag(int) to specify a position for the active view. -->
<item name="mdActiveViewPosition" type="id"/>
<item name="mdItem" type="id"/>
<item name="md__translationX" type="id"/>
<item name="md__translationY" type="id"/>
</resources>
\ No newline at end of file
......@@ -4,15 +4,17 @@
<string name="app_name">Shadowsocks</string>
<string name="screen_name">shadowsocks</string>
<!-- function category -->
<string name="function_cat">Service Controller</string>
<string name="service_controller">Proxy Switch</string>
<string name="service_summary">Enable / Disable Proxy</string>
<!-- global category -->
<string name="global_cat">Global Settings</string>
<string name="profile">Profile</string>
<string name="profile_summary">Switch to another profile</string>
<string name="nat">NAT mode</string>
<string name="nat_summary">Use NAT/VPN mode.</string>
<string name="nat_summary_no_root">NAT mode is disabled without root.</string>
<!-- proxy category -->
<string name="proxy_cat">Server Settings</string>
<string name="profile">Profile</string>
<string name="profile_summary">Profile name</string>
<string name="profile_name">Profile name</string>
<string name="proxy">Server</string>
<string name="proxy_summary">Hostname or IP of the remote server</string>
<string name="remote_port">Remote Port</string>
......@@ -54,26 +56,16 @@
<!-- notification category -->
<string name="forward_success">Shadowsocks started.</string>
<string name="service_running">Connected to "%s"</string>
<string name="service_status" formatted="false">Current Rate: %d KB/s</string>
<string name="forward_fail">Shadowsocks failed to start.</string>
<string name="service_stopped">Shadowsocks stopped.</string>
<string name="forward_stop">Shadowsocks stopped.</string>
<string name="service_failed">Failed to connect the remote server</string>
<string name="stop">Stop the service</string>
<string name="stopping">Shutting down...</string>
<string name="stopping">Shutting down</string>
<string name="vpn_error">%s</string>
<string name="enable_nat">Turn on NAT mode.</string>
<string name="disable_nat">Turn off NAT mode.</string>
<string name="error">ERROR</string>
<!-- alert category -->
<string name="ok_iknow">OK</string>
<string name="close">Close</string>
<string name="port_alert">The local port number should be greater than 1024</string>
<string name="port_empty">Port should not be empty</string>
<string name="proxy_empty">Proxy should not be empty</string>
<string name="crash_alert">An unexpected exit detected, the context has been reset.
</string>
<string name="connect">Connect</string>
<string name="initializing">Initializing…</string>
<string name="recovering">Resetting…</string>
......@@ -101,9 +93,9 @@
<!-- profile -->
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string-array name="add_profile_methods">
<item>Scan QR code</item>
<item>Manual Settings</item>
</string-array>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="removed">Removed</string>
<string name="undo">Undo</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
Zirco Browser for Android
Copyright (C) 2010 - 2011 J. Devauchelle and contributors.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 3 as published by the Free Software Foundation.
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.
-->
<resources>
<style name="Toolbar.TitleText" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<style name="Toolbar.Logo" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">24sp</item>
</style>
<style name="Theme.Material" parent="Theme.AppCompat.Light.NoActionBar">
......@@ -51,45 +37,4 @@
<item name="android:windowIsTranslucent">true</item>
</style>
<!-- Variation on the Light theme that turns off the title -->
<style name="Theme.Shadowsocks" parent="Theme.Material">
<item name="menuDrawerStyle">@style/MenuDrawerStyle</item>
</style>
<style name="MenuDrawer"/>
<style name="MenuDrawer.Widget"/>
<style name="MenuDrawer.Widget.Category">
<item name="android:textStyle">bold</item>
<item name="android:textColor">?android:attr/textColorSecondaryInverse</item>
<item name="android:textSize">14sp</item>
<item name="android:textAllCaps">true</item>
<item name="android:gravity">center_vertical</item>
<item name="android:paddingLeft">16dp</item>
<item name="android:background">@drawable/md__category_background</item>
<item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item>
</style>
<style name="MenuDrawer.Widget.Title">
<item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:textAppearance">?android:attr/textAppearance</item>
<item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
<item name="android:textSize">18sp</item>
<item name="android:paddingLeft">16dp</item>
<item name="android:paddingRight">32dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:drawablePadding">16dp</item>
<item name="android:gravity">center_vertical</item>
</style>
<style name="MenuDrawerStyle" parent="Widget.MenuDrawer">
<item name="mdActiveIndicator">@drawable/menu_arrow</item>
<item name="mdMenuSize">250dp</item>
</style>
</resources>
......@@ -2,50 +2,60 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.github.shadowsocks">
<PreferenceCategory android:title="@string/global_cat">
<Preference android:summary="@string/profile_summary" android:title="@string/profile">
<intent android:action="com.github.shadowsocks.ProfileManagerActivity"/>
</Preference>
<com.github.shadowsocks.preferences.NatSwitchPreference android:key="isNAT"
android:title="@string/nat"
android:summary="@string/nat_summary"/>
<SwitchPreference
android:key="isAutoConnect"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<Preference android:key="recovery" android:title="@string/recovery"/>
<Preference android:key="flush_dnscache" android:title="@string/flush_dnscache"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/proxy_cat">
<com.github.shadowsocks.preferences.ProfileEditTextPreference
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:defaultValue="Default"
android:key="profileName"
android:summary="@string/profile_summary"
android:title="@string/profile">
</com.github.shadowsocks.preferences.ProfileEditTextPreference>
android:title="@string/profile_name"/>
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:defaultValue="198.199.101.152"
android:key="proxy"
android:summary="@string/proxy_summary"
android:title="@string/proxy">
</com.github.shadowsocks.preferences.SummaryEditTextPreference>
android:title="@string/proxy"/>
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:inputType="number"
android:defaultValue="443"
android:key="remotePort"
android:summary="@string/remote_port_summary"
android:title="@string/remote_port">
</com.github.shadowsocks.preferences.SummaryEditTextPreference>
android:title="@string/remote_port"/>
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:inputType="number"
android:defaultValue="1080"
android:key="port"
android:summary="@string/port_summary"
android:title="@string/port">
</com.github.shadowsocks.preferences.SummaryEditTextPreference>
android:title="@string/port"/>
<com.github.shadowsocks.preferences.PasswordEditTextPreference
android:inputType="textPassword"
android:defaultValue="u1rRWTssNv0p"
android:key="sitekey"
android:summary="@string/sitekey_summary"
android:title="@string/sitekey">
</com.github.shadowsocks.preferences.PasswordEditTextPreference>
android:title="@string/sitekey"/>
<com.github.shadowsocks.preferences.DropDownPreference
android:defaultValue="rc4"
android:key="encMethod"
app:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value"
app:summary="%s"
android:title="@string/enc_method">
</com.github.shadowsocks.preferences.DropDownPreference>
android:title="@string/enc_method"/>
</PreferenceCategory>
......@@ -58,21 +68,18 @@
app:entries="@array/route_entry"
app:entryValues="@array/route_value"
app:summary="%s"
android:title="@string/route_list">
</com.github.shadowsocks.preferences.DropDownPreference>
android:title="@string/route_list"/>
<SwitchPreference
android:key="isIpv6"
android:defaultValue="false"
android:summary="@string/ipv6_summary"
android:title="@string/ipv6">
</SwitchPreference>
android:title="@string/ipv6"/>
<SwitchPreference
android:defaultValue="true"
android:key="isGlobalProxy"
android:disableDependentsState="true"
android:summary="@string/auto_set_proxy_summary"
android:title="@string/auto_set_proxy">
</SwitchPreference>
android:title="@string/auto_set_proxy"/>
<Preference
android:key="proxyedApps"
android:dependency="isGlobalProxy"
......@@ -84,19 +91,12 @@
android:key="isUdpDns"
android:defaultValue="false"
android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns">
</SwitchPreference>
android:title="@string/udp_dns"/>
<SwitchPreference
android:key="isAuth"
android:defaultValue="false"
android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth">
</SwitchPreference>
<SwitchPreference
android:key="isAutoConnect"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect">
</SwitchPreference>
android:title="@string/onetime_auth"/>
</PreferenceCategory>
......
......@@ -39,38 +39,28 @@
package com.github.shadowsocks
import android.app.{Activity, ProgressDialog}
import android.content.ClipboardManager
import android.content.ClipData
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.{Bitmap, PixelFormat}
import android.os.Bundle
import android.os.Handler
import android.content.{ClipData, ClipboardManager, Context, SharedPreferences}
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.{Bundle, Handler}
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.WindowManager
import android.widget._
import android.view.{MenuItem, View, ViewGroup, WindowManager}
import android.widget.AbsListView.OnScrollListener
import android.widget.CompoundButton.OnCheckedChangeListener
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import java.io.{ByteArrayOutputStream, ByteArrayInputStream, InputStream}
import com.nostra13.universalimageloader.core.{DisplayImageOptions, ImageLoader, ImageLoaderConfiguration}
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer
import com.github.shadowsocks.utils.{Utils, Scheme, Key}
import android.widget._
import com.github.shadowsocks.utils.{Key, Utils}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.language.implicitConversions
case class ProxiedApp(uid: Int, name: String, packageName: String, var proxied: Boolean)
case class ProxiedApp(uid: Int, name: String, packageName: String, icon: Drawable, var proxied: Boolean)
class ObjectArrayTools[T <: AnyRef](a: Array[T]) {
def binarySearch(key: T) = {
......@@ -100,12 +90,13 @@ object AppManager {
val name = packageManager.getApplicationLabel(a).toString
val packageName = a.packageName
val proxied = proxiedApps.binarySearch(userName) >= 0
new ProxiedApp(uid, name, packageName, proxied)
new ProxiedApp(uid, name, packageName, null, proxied)
}.toArray
}
}
class AppManager extends Activity with OnCheckedChangeListener with OnClickListener {
class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnClickListener
with OnMenuItemClickListener {
val MSG_LOAD_START = 1
val MSG_LOAD_FINISH = 2
......@@ -116,13 +107,11 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
var apps: Array[ProxiedApp] = null
var appListView: ListView = null
var overlay: TextView = null
var progressDialog: ProgressDialog = null
var adapter: ListAdapter = null
var appsLoaded: Boolean = false
def loadApps(context: Context): Array[ProxiedApp] = {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val proxiedAppString = prefs.getString(Key.proxied, "")
val proxiedAppString = ShadowsocksApplication.settings.getString(Key.proxied, "")
val proxiedApps = proxiedAppString.split('|').sortWith(_ < _)
import scala.collection.JavaConversions._
......@@ -139,7 +128,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
val name = packageManager.getApplicationLabel(a).toString
val packageName = a.packageName
val proxied = (proxiedApps binarySearch userName) >= 0
new ProxiedApp(uid, name, packageName, proxied)
new ProxiedApp(uid, name, packageName, a.loadIcon(packageManager), proxied)
}.toArray
}
......@@ -174,19 +163,9 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
}
val app: ProxiedApp = apps(position)
val options =
new DisplayImageOptions.Builder()
.showStubImage(STUB)
.showImageForEmptyUri(STUB)
.showImageOnFail(STUB)
.resetViewBeforeLoading()
.cacheInMemory()
.cacheOnDisc()
.displayer(new FadeInBitmapDisplayer(300))
.build()
ImageLoader.getInstance().displayImage(Scheme.APP + app.packageName, entry.icon, options)
entry.text.setText(app.name)
entry.icon.setImageDrawable(app.icon)
val switch = entry.switch
switch.setTag(app)
switch.setChecked(app.proxied)
......@@ -224,7 +203,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
}
}
protected override def onOptionsItemSelected(item: MenuItem): Boolean = {
def onMenuItemClick(item: MenuItem): Boolean = {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext)
item.getItemId match {
......@@ -262,12 +241,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
return false
}
super.onOptionsItemSelected(item)
}
protected override def onCreateOptionsMenu(menu: Menu): Boolean = {
getMenuInflater.inflate(R.menu.app_manager_menu, menu)
super.onCreateOptionsMenu(menu)
false
}
protected override def onCreate(savedInstanceState: Bundle) {
......@@ -283,6 +257,9 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
val intent = getParentActivityIntent
if (intent == null) finish else navigateUpTo(intent)
})
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
this.overlay = View.inflate(this, R.layout.overlay, null).asInstanceOf[TextView]
getWindowManager.addView(overlay, new
WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
......@@ -290,100 +267,59 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT))
val config =
new ImageLoaderConfiguration.Builder(this)
.imageDownloader(new AppIconDownloader(this))
.build()
ImageLoader.getInstance().init(config)
val bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext)
bypassSwitch.setOnCheckedChangeListener((button: CompoundButton, checked: Boolean) =>
prefs.edit().putBoolean(Key.isBypassApps, checked).apply())
bypassSwitch.setChecked(prefs.getBoolean(Key.isBypassApps, false))
ShadowsocksApplication.settings.edit().putBoolean(Key.isBypassApps, checked).apply())
bypassSwitch.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isBypassApps, false))
appListView = findViewById(R.id.applistview).asInstanceOf[ListView]
appListView.setOnScrollListener(new AbsListView.OnScrollListener {
var visible = false
def onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int,
totalItemCount: Int) {
if (visible) {
val name: String = apps(firstVisibleItem).name
if (name != null && name.length > 1) {
overlay.setText(apps(firstVisibleItem).name.substring(0, 1))
} else {
overlay.setText("*")
}
overlay.setVisibility(View.VISIBLE)
}
}
def onScrollStateChanged(view: AbsListView, scrollState: Int) {
visible = true
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
overlay.setVisibility(View.INVISIBLE)
}
}
})
}
protected override def onResume() {
super.onResume()
Future {
handler.post(loadStartRunnable)
if (!appsLoaded) loadApps()
handler.post(loadFinishRunnable)
handler.post(() => {
appListView.setAdapter(adapter)
Utils.crossFade(AppManager.this, findViewById(R.id.loading), appListView)
})
}
}
def saveAppSettings(context: Context) {
if (apps == null) return
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val proxiedApps = new StringBuilder
apps.foreach(app =>
if (app.proxied) {
proxiedApps ++= app.uid.toString
proxiedApps += '|'
})
val edit: SharedPreferences.Editor = prefs.edit
val edit: SharedPreferences.Editor = ShadowsocksApplication.settings.edit
edit.putString(Key.proxied, proxiedApps.toString())
edit.apply
}
val loadStartRunnable = new Runnable {
override def run() {
progressDialog = ProgressDialog
.show(AppManager.this, "", getString(R.string.loading), true, true)
}
}
val loadFinishRunnable = new Runnable {
override def run() = {
appListView.setAdapter(adapter)
appListView.setOnScrollListener(new AbsListView.OnScrollListener {
def onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int,
totalItemCount: Int) {
if (visible) {
val name: String = apps(firstVisibleItem).name
if (name != null && name.length > 1) {
overlay.setText(apps(firstVisibleItem).name.substring(0, 1))
} else {
overlay.setText("*")
}
overlay.setVisibility(View.VISIBLE)
}
}
def onScrollStateChanged(view: AbsListView, scrollState: Int) {
visible = true
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
overlay.setVisibility(View.INVISIBLE)
}
}
var visible = false
})
if (progressDialog != null) {
progressDialog.dismiss()
progressDialog = null
}
}
}
var handler: Handler = null
class AppIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int)
extends BaseImageDownloader(context, connectTimeout, readTimeout) {
def this(context: Context) {
this(context, 0, 0)
}
override def getStreamFromOtherSource(imageUri: String, extra: AnyRef): InputStream = {
val packageName = imageUri.substring(Scheme.APP.length)
val drawable = Utils.getAppIcon(getBaseContext, packageName)
val bitmap = Utils.drawableToBitmap(drawable)
val os = new ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
new ByteArrayInputStream(os.toByteArray)
}
}
}
......@@ -57,14 +57,14 @@ class ParserActivity extends Activity {
new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setCancelable(false)
.setPositiveButton(R.string.yes, ((dialog: DialogInterface, id: Int) => {
.setPositiveButton(android.R.string.yes, ((dialog: DialogInterface, id: Int) => {
Parser.parse(data) match {
case Some(profile) => addProfile(profile)
case _ => // ignore
}
dialog.dismiss()
}): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((dialog: DialogInterface, id: Int) => {
.setNegativeButton(android.R.string.no, ((dialog: DialogInterface, id: Int) => {
dialog.dismiss()
finish()
}): DialogInterface.OnClickListener)
......@@ -90,7 +90,7 @@ class ParserActivity extends Activity {
h.postDelayed(() => {
val profileManager =
new ProfileManager(PreferenceManager.getDefaultSharedPreferences(getBaseContext),
getApplication.asInstanceOf[ShadowsocksApplication].dbHelper)
ShadowsocksApplication.dbHelper)
profileManager.createOrUpdateProfile(profile)
profileManager.reload(profile.id)
h.sendEmptyMessage(0)
......
package com.github.shadowsocks
import android.app.AlertDialog
import android.content.{DialogInterface, Intent}
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view.View.{OnAttachStateChangeListener, OnClickListener}
import android.view.{LayoutInflater, MenuItem, View, ViewGroup}
import android.widget.{LinearLayout, ImageView, CheckedTextView, Toast}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Parser, Utils}
import com.google.zxing.integration.android.IntentIntegrator
import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.ArrayBuffer
/**
* @author Mygod
*/
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener {
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
itemView.setOnClickListener(this)
{
val qrcode = itemView.findViewById(R.id.qrcode)
qrcode.setOnClickListener((v: View) => {
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(Parser.generate(item))
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode]
image.setImageBitmap(qrcode.bitmap())
new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setNegativeButton(getString(R.string.close),
((dialog: DialogInterface, id: Int) => dialog.cancel()): DialogInterface.OnClickListener)
.setView(image)
.create()
.show()
})
qrcode.setOnLongClickListener((v: View) => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.qrcode, Toast.LENGTH_SHORT), qrcode,
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show
true
})
}
def bind(item: Profile) {
text.setText(item.name)
text.setChecked(item.id == ShadowsocksApplication.profileId)
this.item = item
}
def onClick(v: View) = {
ShadowsocksApplication.profileId(item.id)
finish
}
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
private val recycleBin = new ArrayBuffer[(Int, Profile)]
private var profiles = new ArrayBuffer[Profile]
profiles ++= ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List[Profile]())
def getItemCount = profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
def add(item: Profile) {
removedSnackbar.dismiss
commitRemoves
val pos = getItemCount
if (ShadowsocksApplication.profileManager.createOrUpdateProfile(item)) {
profiles += item
notifyItemInserted(pos)
}
}
def remove(pos: Int) {
recycleBin.append((pos, profiles(pos)))
profiles.remove(pos)
notifyItemRemoved(pos)
}
def undoRemoves {
for ((index, item) <- recycleBin.reverseIterator) {
profiles.insert(index, item)
notifyItemInserted(index)
}
recycleBin.clear
}
def commitRemoves {
for ((index, item) <- recycleBin) {
ShadowsocksApplication.profileManager.delProfile(item.id)
if (item.id == ShadowsocksApplication.profileId) ShadowsocksApplication.profileId(-1)
}
recycleBin.clear
}
}
private lazy val profilesAdapter = new ProfilesAdapter
private var removedSnackbar: Snackbar = _
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profiles)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.profiles)
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha)
toolbar.setNavigationOnClickListener((v: View) => {
val intent = getParentActivityIntent
if (intent == null) finish else navigateUpTo(intent)
})
toolbar.inflateMenu(R.menu.add_profile_methods)
toolbar.setOnMenuItemClickListener(this)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
profilesList.setLayoutManager(new LinearLayoutManager(this))
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
removedSnackbar = Snackbar.make(findViewById(android.R.id.content), R.string.removed, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, ((v: View) => profilesAdapter.undoRemoves): OnClickListener)
removedSnackbar.getView.addOnAttachStateChangeListener(new OnAttachStateChangeListener {
def onViewDetachedFromWindow(v: View) = profilesAdapter.commitRemoves
def onViewAttachedToWindow(v: View) = ()
})
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
def onSwiped(viewHolder: ViewHolder, direction: Int) = {
profilesAdapter.remove(viewHolder.getAdapterPosition)
removedSnackbar.show
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = false // TODO?
}).attachToRecyclerView(profilesList)
}
override def onDestroy {
super.onDestroy
profilesAdapter.commitRemoves
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) Parser.parse(scanResult.getContents) match {
case Some(profile) => profilesAdapter.add(profile)
case _ => // ignore
}
}
def onMenuItemClick(item: MenuItem) = item.getItemId match {
case R.id.scan_qr_code =>
val integrator = new IntentIntegrator(this)
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
list.add("tw.com.quickmark")
integrator.setTargetApplications(list)
integrator.initiateScan()
true
case R.id.manual_settings =>
ShadowsocksApplication.profileManager.reload(-1)
ShadowsocksApplication.profileId(ShadowsocksApplication.profileManager.save.id)
finish
true
case _ => false
}
}
......@@ -38,66 +38,37 @@
*/
package com.github.shadowsocks
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, FileOutputStream, IOException, InputStream, OutputStream}
import java.io.{FileOutputStream, IOException, InputStream, OutputStream}
import java.util
import java.util.Locale
import android.app.backup.BackupManager
import android.app.{Activity, AlertDialog, ProgressDialog}
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.content.res.AssetManager
import android.graphics.{Bitmap, Color, Typeface}
import android.graphics.Typeface
import android.net.{Uri, VpnService}
import android.os._
import android.preference.{Preference, PreferenceManager, SwitchPreference}
import android.support.v4.content.ContextCompat
import android.preference.{Preference, SwitchPreference}
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.util.{DisplayMetrics, Log}
import android.util.Log
import android.view.{View, ViewGroup, ViewParent}
import android.webkit.{WebView, WebViewClient}
import android.widget._
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database._
import com.github.shadowsocks.preferences.{DropDownPreference, PasswordEditTextPreference, ProfileEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.preferences.{DropDownPreference, PasswordEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import com.google.android.gms.analytics.HitBuilders
import com.google.zxing.integration.android.IntentIntegrator
import com.joanzapata.android.iconify.IconDrawable
import com.joanzapata.android.iconify.Iconify.IconValue
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import net.glxn.qrgen.android.QRCode
import net.simonvt.menudrawer.MenuDrawer
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class ProfileIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int)
extends BaseImageDownloader(context, connectTimeout, readTimeout) {
def this(context: Context) {
this(context, 0, 0)
}
override def getStreamFromOtherSource(imageUri: String, extra: AnyRef): InputStream = {
val text = imageUri.substring(Scheme.PROFILE.length)
val size = Utils.dpToPx(context, 16).toInt
val idx = text.getBytes.last % 6
val color = Seq(Color.MAGENTA, Color.GREEN, Color.YELLOW, Color.BLUE, Color.DKGRAY, Color.CYAN)(
idx)
val bitmap = Utils.getBitmap(text, size, size, color)
val os = new ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
new ByteArrayInputStream(os.toByteArray)
}
}
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
cache synchronized {
......@@ -126,12 +97,10 @@ object Shadowsocks {
val REQUEST_CONNECT = 1
val PREFS_NAME = "Shadowsocks"
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey,
Key.encMethod)
val FEATRUE_PREFS = Array(Key.route, Key.isGlobalProxy, Key.proxyedApps,
Key.isUdpDns, Key.isAuth, Key.isIpv6, Key.isAutoConnect)
val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL, Executable.TUN2SOCKS)
val PROXY_PREFS = Array(Key.proxy, Key.remotePort, Key.localPort, Key.sitekey, Key.encMethod)
val FEATURE_PREFS = Array(Key.route, Key.isGlobalProxy, Key.proxyedApps, Key.isUdpDns, Key.isAuth, Key.isIpv6)
val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL,
Executable.TUN2SOCKS)
// Helper functions
def updateDropDownPreference(pref: Preference, value: String) {
......@@ -148,18 +117,13 @@ object Shadowsocks {
pref.asInstanceOf[SummaryEditTextPreference].setText(value)
}
def updateProfileEditTextPreference(pref: Preference, value: String) {
pref.asInstanceOf[ProfileEditTextPreference].resetSummary(value)
pref.asInstanceOf[ProfileEditTextPreference].setText(value)
}
def updateSwitchPreference(pref: Preference, value: Boolean) {
pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile) {
name match {
case Key.profileName => updateProfileEditTextPreference(pref, profile.name)
case Key.profileName => updateSummaryEditTextPreference(pref, profile.name)
case Key.proxy => updateSummaryEditTextPreference(pref, profile.host)
case Key.remotePort => updateSummaryEditTextPreference(pref, profile.remotePort.toString)
case Key.localPort => updateSummaryEditTextPreference(pref, profile.localPort.toString)
......@@ -176,14 +140,8 @@ object Shadowsocks {
}
class Shadowsocks
extends AppCompatActivity
with MenuAdapter.MenuListener {
// Flags
val MSG_CRASH_RECOVER: Int = 1
val STATE_MENUDRAWER = "com.github.shadowsocks.menuDrawer"
val STATE_ACTIVE_VIEW_ID = "com.github.shadowsocks.activeViewId"
var singlePane: Int = -1
extends AppCompatActivity {
import Shadowsocks._
// Variables
private var serviceStarted = false
......@@ -217,8 +175,8 @@ class Shadowsocks
if (fab != null) fab.setEnabled(true)
stateUpdate()
if (!status.getBoolean(getVersionName, false)) {
status.edit.putBoolean(getVersionName, true).apply()
if (!ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)) {
ShadowsocksApplication.settings.edit.putBoolean(ShadowsocksApplication.getVersionName, true).apply()
recovery()
}
}
......@@ -236,33 +194,11 @@ class Shadowsocks
private lazy val preferences =
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
lazy val settings = PreferenceManager.getDefaultSharedPreferences(this)
lazy val status = getSharedPreferences(Key.status, Context.MODE_PRIVATE)
private lazy val preferenceReceiver = new PreferenceBroadcastReceiver
private lazy val drawer = MenuDrawer.attach(this)
private lazy val menuAdapter = new MenuAdapter(this, getMenuList)
private lazy val listView = new ListView(this)
private lazy val profileManager =
new ProfileManager(settings, getApplication.asInstanceOf[ShadowsocksApplication].dbHelper)
private lazy val application = getApplication.asInstanceOf[ShadowsocksApplication]
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_600)
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
var handler = new Handler()
def isSinglePane: Boolean = {
if (singlePane == -1) {
val metrics = new DisplayMetrics()
getWindowManager.getDefaultDisplay.getMetrics(metrics)
val widthPixels = metrics.widthPixels
val scaleFactor = metrics.density
val widthDp = widthPixels / scaleFactor
singlePane = if (widthDp <= 720) 1 else 0
}
singlePane == 1
}
private def changeSwitch(checked: Boolean) {
serviceStarted = checked
fab.setImageResource(if (checked) R.drawable.ic_cloud else R.drawable.ic_cloud_off)
......@@ -359,18 +295,6 @@ class Shadowsocks
Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
}
private def getVersionName: String = {
var version: String = null
try {
val pi: PackageInfo = getPackageManager.getPackageInfo(getPackageName, 0)
version = pi.versionName
} catch {
case e: PackageManager.NameNotFoundException =>
version = "Package name not found"
}
version
}
def isTextEmpty(s: String, msg: String): Boolean = {
if (s != null && s.length > 0) return false
Snackbar.make(getWindow.getDecorView.findViewById(android.R.id.content), msg, Snackbar.LENGTH_LONG).show
......@@ -393,7 +317,7 @@ class Shadowsocks
def prepareStartService() {
Future {
if (isVpnEnabled) {
if (ShadowsocksApplication.isVpnEnabled) {
val intent = VpnService.prepare(this)
if (intent != null) {
startActivityForResult(intent, Shadowsocks.REQUEST_CONNECT)
......@@ -413,60 +337,43 @@ class Shadowsocks
}
}
override def setContentView(layoutResId: Int) {
drawer.setContentView(layoutResId)
onContentChanged()
}
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
if (settings.getString(Key.proxy, "") == "198.199.101.152") {
setContentView(R.layout.layout_main_ad)
val adView = findViewById(R.id.adView).asInstanceOf[AdView]
setContentView(R.layout.layout_main)
if (ShadowsocksApplication.proxy == "198.199.101.152") {
val adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 0)
adView.loadAd(new AdRequest.Builder().build())
} else {
setContentView(R.layout.layout_main)
}
// Update the profile
if (!status.getBoolean(getVersionName, false)) {
currentProfile = profileManager.create()
if (!ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)) {
currentProfile = ShadowsocksApplication.profileManager.create()
}
// Initialize the profile
currentProfile = {
profileManager.getProfile(settings.getInt(Key.profileId, -1)) getOrElse currentProfile
}
// Initialize drawer
menuAdapter.setActiveId(settings.getInt(Key.profileId, -1))
menuAdapter.setListener(this)
listView.setAdapter(menuAdapter)
drawer.setMenuView(listView)
if (Utils.isLollipopOrAbove) {
drawer.setDrawerIndicatorEnabled(false)
} else {
// The drawable that replaces the up indicator in the action bar
drawer.setSlideDrawable(R.drawable.ic_drawer)
// Whether the previous drawable should be shown
drawer.setDrawerIndicatorEnabled(true)
}
if (!isSinglePane) {
drawer.openMenu(false)
ShadowsocksApplication.currentProfile getOrElse currentProfile
}
// Initialize action bar
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getTitle)
toolbar.setTitle(getString(R.string.screen_name))
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
val field = classOf[Toolbar].getDeclaredField("mTitleTextView")
field.setAccessible(true)
val title: TextView = field.get(toolbar).asInstanceOf[TextView]
val tf: Typeface = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
title.setOnClickListener((view: View) => {
ShadowsocksApplication.track(TAG, "about")
showAbout()
})
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener((v: View) => {
......@@ -489,29 +396,9 @@ class Shadowsocks
})
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).toInt).show
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show
true
})
toolbar.setNavigationIcon(R.drawable.ic_drawer)
toolbar.setNavigationOnClickListener((v: View) => drawer.toggleMenu())
title.setOnLongClickListener((v: View) => {
if (Utils.isLollipopOrAbove && bgService != null
&& (bgService.getState == State.INIT || bgService.getState == State.STOPPED)) {
val natEnabled = !status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)
status.edit().putBoolean(Key.isNAT, natEnabled).apply()
deattachService()
attachService()
if (!Utils.isLollipopOrAbove) setPreferenceEnabled(State.isAvailable(bgService.getState))
Toast.makeText(getBaseContext, if (natEnabled) R.string.enable_nat else R.string.disable_nat, Toast.LENGTH_LONG)
.show()
true
} else {
false
}
})
// Register broadcast receiver
registerReceiver(preferenceReceiver, new IntentFilter(Action.UPDATE_PREFS))
// Bind to the service
handler.post(() => {
......@@ -521,7 +408,8 @@ class Shadowsocks
def attachService() {
if (bgService == null) {
val s = if (!isVpnEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
val s = if (ShadowsocksApplication.isVpnEnabled) classOf[ShadowsocksVpnService]
else classOf[ShadowsocksNatService]
val intent = new Intent(this, s)
intent.setAction(Action.SERVICE)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
......@@ -541,222 +429,28 @@ class Shadowsocks
}
}
override def onRestoreInstanceState(inState: Bundle) {
super.onRestoreInstanceState(inState)
drawer.restoreState(inState.getParcelable(STATE_MENUDRAWER))
}
override def onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(STATE_MENUDRAWER, drawer.saveState())
outState.putInt(STATE_ACTIVE_VIEW_ID, currentProfile.id)
}
override def onBackPressed() {
val drawerState = drawer.getDrawerState
if (drawerState == MenuDrawer.STATE_OPEN || drawerState == MenuDrawer.STATE_OPENING) {
drawer.closeMenu()
return
}
super.onBackPressed()
}
override def onActiveViewChanged(v: View, pos: Int) {
drawer.setActiveView(v, pos)
}
def newProfile(id: Int) {
val builder = new AlertDialog.Builder(this)
builder
.setTitle(R.string.add_profile)
.setItems(R.array.add_profile_methods, ((dialog: DialogInterface, which: Int) => which match {
case 0 =>
dialog.dismiss()
val h = showProgress(R.string.loading)
h.postDelayed(() => {
val integrator = new IntentIntegrator(Shadowsocks.this)
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
list.add("tw.com.quickmark")
integrator.setTargetApplications(list)
integrator.initiateScan()
h.sendEmptyMessage(0)
}, 600)
case 1 =>
dialog.dismiss()
addProfile(id)
case _ =>
}): DialogInterface.OnClickListener)
builder.create().show()
}
def reloadProfile() {
drawer.closeMenu(true)
val h = showProgress(R.string.loading)
handler.postDelayed(() => {
currentProfile = {
profileManager.getProfile(settings.getInt(Key.profileId, -1)) getOrElse currentProfile
}
menuAdapter.updateList(getMenuList, currentProfile.id)
updatePreferenceScreen()
currentProfile = ShadowsocksApplication.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List[Profile]())
ShadowsocksApplication.profileId(if (profiles.isEmpty) -1 else profiles.head.id)
}
h.sendEmptyMessage(0)
}, 600)
updatePreferenceScreen()
}
def addProfile(profile: Profile) {
drawer.closeMenu(true)
val h = showProgress(R.string.loading)
handler.postDelayed(() => {
currentProfile = profile
profileManager.createOrUpdateProfile(currentProfile)
profileManager.reload(currentProfile.id)
menuAdapter.updateList(getMenuList, currentProfile.id)
updatePreferenceScreen()
h.sendEmptyMessage(0)
}, 600)
}
def addProfile(id: Int) {
drawer.closeMenu(true)
val h = showProgress(R.string.loading)
handler.postDelayed(() => {
currentProfile = profileManager.reload(id)
profileManager.save()
menuAdapter.updateList(getMenuList, currentProfile.id)
currentProfile = profile
ShadowsocksApplication.profileManager.createOrUpdateProfile(currentProfile)
ShadowsocksApplication.profileManager.reload(currentProfile.id)
updatePreferenceScreen()
h.sendEmptyMessage(0)
}, 600)
}
def updateProfile(id: Int) {
drawer.closeMenu(true)
val h = showProgress(R.string.loading)
handler.postDelayed(() => {
currentProfile = profileManager.reload(id)
menuAdapter.setActiveId(id)
menuAdapter.notifyDataSetChanged()
updatePreferenceScreen()
h.sendEmptyMessage(0)
}, 600)
}
def delProfile(id: Int): Boolean = {
drawer.closeMenu(true)
val profile = profileManager.getProfile(id)
if (profile.isEmpty) return false
new AlertDialog.Builder(this)
.setMessage(String.format(Locale.ENGLISH, getString(R.string.remove_profile), profile.get.name))
.setCancelable(false)
.setNegativeButton(R.string.no,
((dialog: DialogInterface, i: Int) => dialog.cancel()): DialogInterface.OnClickListener)
.setPositiveButton(R.string.yes, ((dialog: DialogInterface, i: Int) => {
profileManager.delProfile(id)
val profileId = {
val profiles = profileManager.getAllProfiles.getOrElse(List[Profile]())
if (profiles.isEmpty) -1 else profiles.head.id
}
currentProfile = profileManager.load(profileId)
menuAdapter.updateList(getMenuList, currentProfile.id)
updatePreferenceScreen()
dialog.dismiss()
}): DialogInterface.OnClickListener)
.create()
.show()
true
}
def getProfileList: List[Item] = {
val list = profileManager.getAllProfiles getOrElse List[Profile]()
list.map(p => new IconItem(p.id, p.name, -1, updateProfile, delProfile))
}
def getMenuList: List[Any] = {
val buf = new ListBuffer[Any]()
buf += new Category(getString(R.string.profiles))
buf ++= getProfileList
buf +=
new DrawableItem(-400, getString(R.string.add_profile), new IconDrawable(this, IconValue.fa_plus_circle)
.colorRes(android.R.color.darker_gray).sizeDp(26), newProfile)
buf += new Category(getString(R.string.settings))
buf += new DrawableItem(-100, getString(R.string.recovery), new IconDrawable(this, IconValue.fa_recycle)
.colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
.setAction("reset")
.setLabel(getVersionName)
.build())
recovery()
})
buf +=
new DrawableItem(-200, getString(R.string.flush_dnscache), new IconDrawable(this, IconValue.fa_refresh)
.colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
.setAction("flush_dnscache")
.setLabel(getVersionName)
.build())
flushDnsCache()
})
buf +=
new DrawableItem(-300, getString(R.string.qrcode), new IconDrawable(this, IconValue.fa_qrcode)
.colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
.setAction("qrcode")
.setLabel(getVersionName)
.build())
showQrCode()
})
buf += new DrawableItem(-400, getString(R.string.about), new IconDrawable(this, IconValue.fa_info_circle)
.colorRes(android.R.color.darker_gray).sizeDp(26), _ => {
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(Shadowsocks.TAG)
.setAction("about")
.setLabel(getVersionName)
.build())
showAbout()
})
buf.toList
updatePreferenceScreen()
}
protected override def onPause() {
super.onPause()
ShadowsocksApplication.profileManager.save
prepared = false
}
......@@ -764,19 +458,19 @@ class Shadowsocks
if (bgService != null) {
bgService.getState match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
fabProgressCircle.show()
changeSwitch(checked = true)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint);
fab.setBackgroundTintList(greenTint)
fabProgressCircle.hide()
changeSwitch(checked = true)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
fabProgressCircle.show()
changeSwitch(checked = false)
case _ =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
fabProgressCircle.hide()
changeSwitch(checked = false)
}
......@@ -789,25 +483,24 @@ class Shadowsocks
stateUpdate()
ConfigUtils.refresh(this)
// Check if profile list changed
val id = settings.getInt(Key.profileId, -1)
if (id != -1 && id != currentProfile.id)
reloadProfile()
// Check if current profile changed
if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile()
}
private def setPreferenceEnabled(enabled: Boolean) {
preferences.findPreference(Key.isNAT).setEnabled(enabled)
for (name <- Shadowsocks.PROXY_PREFS) {
val pref = preferences.findPreference(name)
if (pref != null) {
pref.setEnabled(enabled)
}
}
for (name <- Shadowsocks.FEATRUE_PREFS) {
for (name <- Shadowsocks.FEATURE_PREFS) {
val pref = preferences.findPreference(name)
if (pref != null) {
if (Seq(Key.isGlobalProxy, Key.proxyedApps)
.contains(name)) {
pref.setEnabled(enabled && (Utils.isLollipopOrAbove || !isVpnEnabled))
pref.setEnabled(enabled && (Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
} else {
pref.setEnabled(enabled)
}
......@@ -817,11 +510,12 @@ class Shadowsocks
private def updatePreferenceScreen() {
val profile = currentProfile
Shadowsocks.updatePreference(preferences.findPreference(Key.profileName), Key.profileName, profile)
for (name <- Shadowsocks.PROXY_PREFS) {
val pref = preferences.findPreference(name)
Shadowsocks.updatePreference(pref, name, profile)
}
for (name <- Shadowsocks.FEATRUE_PREFS) {
for (name <- Shadowsocks.FEATURE_PREFS) {
val pref = preferences.findPreference(name)
Shadowsocks.updatePreference(pref, name, profile)
}
......@@ -839,7 +533,6 @@ class Shadowsocks
override def onDestroy() {
super.onDestroy()
deattachService()
unregisterReceiver(preferenceReceiver)
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
}
......@@ -867,13 +560,12 @@ class Shadowsocks
}
def reset() {
crashRecovery()
install()
}
private def recovery() {
def recovery() {
serviceStop()
val h = showProgress(R.string.recovering)
Future {
......@@ -882,26 +574,7 @@ class Shadowsocks
}
}
private def dp2px(dp: Int): Int =
Math.round(dp * (getBaseContext.getResources.getDisplayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT))
private def showQrCode() {
val image = new ImageView(this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(Parser.generate(currentProfile))
.withSize(dp2px(250), dp2px(250)).asInstanceOf[QRCode]
image.setImageBitmap(qrcode.bitmap())
new AlertDialog.Builder(this)
.setCancelable(true)
.setNegativeButton(getString(R.string.close),
((dialog: DialogInterface, id: Int) => dialog.cancel()): DialogInterface.OnClickListener)
.setView(image)
.create()
.show()
}
private def flushDnsCache() {
def flushDnsCache() {
val h = showProgress(R.string.flushing)
Future {
Utils.toggleAirplaneMode(getBaseContext)
......@@ -909,29 +582,13 @@ class Shadowsocks
}
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) {
Parser.parse(scanResult.getContents) match {
case Some(profile) => addProfile(profile)
case _ => // ignore
}
} else {
resultCode match {
case Activity.RESULT_OK =>
prepared = true
serviceStart()
case _ =>
cancelStart()
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
}
}
}
var isRoot: Option[Boolean] = None
def isVpnEnabled: Boolean = {
if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
!(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
case Activity.RESULT_OK =>
prepared = true
serviceStart()
case _ =>
cancelStart()
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
}
def serviceStop() {
......@@ -939,12 +596,12 @@ class Shadowsocks
}
def checkText(key: String): Boolean = {
val text = settings.getString(key, "")
val text = ShadowsocksApplication.settings.getString(key, "")
!isTextEmpty(text, getString(R.string.proxy_empty))
}
def checkNumber(key: String, low: Boolean): Boolean = {
val text = settings.getString(key, "")
val text = ShadowsocksApplication.settings.getString(key, "")
if (isTextEmpty(text, getString(R.string.port_empty))) return false
try {
val port: Int = Integer.valueOf(text)
......@@ -964,15 +621,14 @@ class Shadowsocks
/** Called when connect button is clicked. */
def serviceStart() {
bgService.start(ConfigUtils.load(settings))
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
if (isVpnEnabled) {
if (ShadowsocksApplication.isVpnEnabled) {
changeSwitch(checked = false)
}
}
private def showAbout() {
val web = new WebView(this)
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
......@@ -982,18 +638,10 @@ class Shadowsocks
}
})
var versionName = ""
try {
versionName = getPackageManager.getPackageInfo(getPackageName, 0).versionName
} catch {
case ex: PackageManager.NameNotFoundException =>
versionName = ""
}
new AlertDialog.Builder(this)
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, versionName))
.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, ShadowsocksApplication.getVersionName))
.setCancelable(false)
.setNegativeButton(getString(R.string.ok_iknow),
.setNegativeButton(getString(android.R.string.ok),
((dialog: DialogInterface, id: Int) => dialog.cancel()): DialogInterface.OnClickListener)
.setView(web)
.create()
......@@ -1012,13 +660,13 @@ class Shadowsocks
handler.post(() => if (state != s) {
s match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_cloud_queue)
fab.setEnabled(false)
fabProgressCircle.show()
setPreferenceEnabled(enabled = false)
case State.CONNECTED =>
fab.setBackgroundTintList(greenTint);
fab.setBackgroundTintList(greenTint)
if (state == State.CONNECTING) {
fabProgressCircle.beginFinalAnimation()
} else {
......@@ -1028,7 +676,7 @@ class Shadowsocks
changeSwitch(checked = true)
setPreferenceEnabled(enabled = false)
case State.STOPPED =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
fab.setEnabled(true)
changeSwitch(checked = false)
......@@ -1036,21 +684,13 @@ class Shadowsocks
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show
setPreferenceEnabled(enabled = true)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint);
fab.setBackgroundTintList(greyTint)
fab.setImageResource(R.drawable.ic_cloud_queue)
fab.setEnabled(false)
fabProgressCircle.show()
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
setPreferenceEnabled(enabled = false)
}
state = s
})
}
class PreferenceBroadcastReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) {
currentProfile = profileManager.save()
menuAdapter.updateList(getMenuList, currentProfile.id)
}
}
}
......@@ -43,19 +43,53 @@ import java.util
import java.util.concurrent.TimeUnit
import android.app.Application
import com.github.shadowsocks.database.DBHelper
import com.github.shadowsocks.utils.Utils
import com.google.android.gms.analytics.GoogleAnalytics
import android.content.pm.PackageManager
import android.preference.PreferenceManager
import com.github.shadowsocks.database.{DBHelper, ProfileManager}
import com.github.shadowsocks.utils.{Console, Key, Utils}
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders}
import com.google.android.gms.common.api.ResultCallback
import com.google.android.gms.tagmanager.{ContainerHolder, TagManager}
object ShadowsocksApplication {
var instance: ShadowsocksApplication = _
lazy val dbHelper = new DBHelper(instance)
final val SIG_FUNC = "getSignature"
var containerHolder: ContainerHolder = _
lazy val tracker = GoogleAnalytics.getInstance(instance).newTracker(R.xml.tracker)
lazy val settings = PreferenceManager.getDefaultSharedPreferences(instance)
lazy val profileManager = new ProfileManager(settings, dbHelper)
val isRoot = Console.isRoot
def isVpnEnabled = !(isRoot && settings.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
def getVersionName = try {
instance.getPackageManager.getPackageInfo(instance.getPackageName, 0).versionName
} catch {
case _: PackageManager.NameNotFoundException => "Package name not found"
case _: Throwable => null
}
// send event
def track(category: String, action: String) = tracker.send(new HitBuilders.EventBuilder()
.setAction(action)
.setLabel(getVersionName)
.build())
def profileId = settings.getInt(Key.profileId, -1)
def profileId(i: Int) = {
settings.edit.putInt(Key.profileId, i).apply
profileManager.load(i)
}
def proxy = settings.getString(Key.proxy, "")
def currentProfile = profileManager.getProfile(profileId)
}
class ShadowsocksApplication extends Application {
lazy val dbHelper = new DBHelper(this)
lazy val SIG_FUNC = "getSignature"
var containerHolder: ContainerHolder = null
lazy val tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
import ShadowsocksApplication._
override def onCreate() {
ShadowsocksApplication.instance = this
val tm = TagManager.getInstance(this)
val pending = tm.loadContainerPreferNonDefault("GTM-NT8WS8", R.raw.gtm_default_container)
val callback = new ResultCallback[ContainerHolder] {
......
......@@ -40,22 +40,20 @@
package com.github.shadowsocks
import java.io.File
import java.lang.reflect.{InvocationTargetException, Method}
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.app._
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.net.{Network, ConnectivityManager}
import android.net.{ConnectivityManager, Network}
import android.os._
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import android.util.{SparseArray, Log}
import android.util.{Log, SparseArray}
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.HitBuilders
import scala.collection._
import scala.collection.mutable.ArrayBuffer
......@@ -77,8 +75,6 @@ class ShadowsocksNatService extends Service with BaseService {
var apps: Array[ProxiedApp] = null
val myUid = Process.myUid()
private lazy val application = getApplication.asInstanceOf[ShadowsocksApplication]
private val dnsAddressCache = new SparseArray[String]
def getNetId(network: Network): Int = {
......@@ -254,8 +250,8 @@ class ShadowsocksNatService extends Service with BaseService {
def startDnsDaemon() {
val conf = if (config.route == Route.BYPASS_CHN) {
val reject = ConfigUtils.getRejectList(getContext, application)
val blackList = ConfigUtils.getBlackList(getContext, application)
val reject = ConfigUtils.getRejectList(getContext)
val blackList = ConfigUtils.getBlackList(getContext)
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "127.0.0.1", 8153,
Path.BASE + "pdnsd-nat.pid", reject, blackList, 8163, "")
} else {
......@@ -457,19 +453,14 @@ class ShadowsocksNatService extends Service with BaseService {
registerReceiver(lockReceiver, screenFilter)
}
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(TAG)
.setAction("start")
.setLabel(getVersionName)
.build())
ShadowsocksApplication.track(TAG, "start")
changeState(State.CONNECTING)
Future {
if (config.proxy == "198.199.101.152") {
val holder = application.containerHolder
val holder = ShadowsocksApplication.containerHolder
try {
config = ConfigUtils.getPublicConfig(getBaseContext, holder.getContainer, config)
} catch {
......@@ -531,12 +522,7 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(TAG)
.setAction("stop")
.setLabel(getVersionName)
.build())
ShadowsocksApplication.track(TAG, "stop")
// reset NAT
killProcesses()
......
......@@ -39,12 +39,7 @@
package com.github.shadowsocks
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.preference.PreferenceManager
import android.content.{BroadcastReceiver, Context, Intent}
import com.github.shadowsocks.utils._
class ShadowsocksReceiver extends BroadcastReceiver {
......@@ -52,18 +47,8 @@ class ShadowsocksReceiver extends BroadcastReceiver {
val TAG = "Shadowsocks"
def onReceive(context: Context, intent: Intent) {
val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val status = context.getSharedPreferences(Key.status, Context.MODE_PRIVATE)
var versionName: String = null
try {
versionName = context.getPackageManager.getPackageInfo(context.getPackageName, 0).versionName
} catch {
case e: PackageManager.NameNotFoundException =>
versionName = "NONE"
}
val isAutoConnect: Boolean = settings.getBoolean(Key.isAutoConnect, false)
val isInstalled: Boolean = status.getBoolean(versionName, false)
val isAutoConnect: Boolean = ShadowsocksApplication.settings.getBoolean(Key.isAutoConnect, false)
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
if (isAutoConnect && isInstalled) {
if (Utils.isLollipopOrAbove) {
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
......
......@@ -40,19 +40,15 @@
package com.github.shadowsocks
import android.app.{Activity, KeyguardManager}
import android.os._
import android.net.VpnService
import android.content._
import android.net.VpnService
import android.os._
import android.util.Log
import android.preference.PreferenceManager
import com.github.shadowsocks.utils._
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.utils._
class ShadowsocksRunnerActivity extends Activity {
private lazy val settings = PreferenceManager.getDefaultSharedPreferences(this)
private lazy val status = getSharedPreferences(Key.status, Context.MODE_PRIVATE)
val handler = new Handler()
val connection = new ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
......@@ -65,18 +61,12 @@ class ShadowsocksRunnerActivity extends Activity {
}
// Variables
var isRoot: Option[Boolean] = None
var bgService: IShadowsocksService = _
var receiver:BroadcastReceiver = _
def isVpnEnabled: Boolean = {
if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
!(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
}
def startBackgroundService() {
if (isVpnEnabled) {
if (ShadowsocksApplication.isVpnEnabled) {
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
if (intent != null) {
startActivityForResult(intent, Shadowsocks.REQUEST_CONNECT)
......@@ -84,14 +74,15 @@ class ShadowsocksRunnerActivity extends Activity {
onActivityResult(Shadowsocks.REQUEST_CONNECT, Activity.RESULT_OK, null)
}
} else {
bgService.start(ConfigUtils.load(settings))
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
finish()
}
}
def attachService() {
if (bgService == null) {
val s = if (!isVpnEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
val s = if (ShadowsocksApplication.isVpnEnabled) classOf[ShadowsocksVpnService]
else classOf[ShadowsocksNatService]
val intent = new Intent(this, s)
intent.setAction(Action.SERVICE)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
......@@ -137,7 +128,7 @@ class ShadowsocksRunnerActivity extends Activity {
resultCode match {
case Activity.RESULT_OK =>
if (bgService != null) {
bgService.start(ConfigUtils.load(settings))
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
}
case _ =>
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
......
......@@ -43,15 +43,10 @@ import android.app.Service
import android.content._
import android.net.VpnService
import android.os._
import android.preference.PreferenceManager
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.utils._
class ShadowsocksRunnerService extends Service {
private lazy val settings = PreferenceManager.getDefaultSharedPreferences(this)
private lazy val status = getSharedPreferences(Key.status, Context.MODE_PRIVATE)
val handler = new Handler()
val connection = new ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
......@@ -64,35 +59,29 @@ class ShadowsocksRunnerService extends Service {
}
// Variables
var isRoot: Option[Boolean] = None
var bgService: IShadowsocksService = _
def isVpnEnabled: Boolean = {
if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
!(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
}
override def onBind(intent: Intent): IBinder = {
null
}
def startBackgroundService() {
if (isVpnEnabled) {
if (ShadowsocksApplication.isVpnEnabled) {
val intent = VpnService.prepare(ShadowsocksRunnerService.this)
if (intent == null) {
if (bgService != null) {
bgService.start(ConfigUtils.load(settings))
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
}
}
} else {
bgService.start(ConfigUtils.load(settings))
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
}
stopSelf()
}
def attachService() {
if (bgService == null) {
val s = if (!isVpnEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
val s = if (!ShadowsocksApplication.isVpnEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
val intent = new Intent(this, s)
intent.setAction(Action.SERVICE)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
......
package com.github.shadowsocks
import android.os.Bundle
import android.preference.PreferenceFragment
import android.preference.{Preference, PreferenceFragment}
import com.github.shadowsocks.utils.Key
// TODO: Move related logic here
class ShadowsocksSettings extends PreferenceFragment {
private lazy val activity = getActivity.asInstanceOf[Shadowsocks]
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.pref_all)
findPreference(Key.isNAT).setOnPreferenceChangeListener((preference: Preference, newValue: Any) =>
if (ShadowsocksApplication.isRoot) activity.handler.post(() => {
activity.deattachService()
activity.attachService()
}) else false)
findPreference("recovery").setOnPreferenceClickListener((preference: Preference) => {
ShadowsocksApplication.track(Shadowsocks.TAG, "reset")
activity.recovery()
true
})
findPreference("flush_dnscache").setOnPreferenceClickListener((preference: Preference) => {
ShadowsocksApplication.track(Shadowsocks.TAG, "flush_dnscache")
activity.flushDnsCache()
true
})
}
}
......@@ -53,7 +53,6 @@ import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.HitBuilders
import org.apache.commons.net.util.SubnetUtils
import scala.collection.mutable
......@@ -62,8 +61,6 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class ShadowsocksVpnService extends VpnService with BaseService {
private lazy val application = getApplication.asInstanceOf[ShadowsocksApplication]
val TAG = "ShadowsocksVpnService"
val VPN_MTU = 1500
val PRIVATE_VLAN = "26.26.26.%s"
......@@ -152,12 +149,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
// channge the state
changeState(State.STOPPING)
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(TAG)
.setAction("stop")
.setLabel(getVersionName)
.build())
ShadowsocksApplication.track(TAG, "stop")
// reset VPN
killProcesses()
......@@ -231,18 +223,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
return
}
// send event
application.tracker.send(new HitBuilders.EventBuilder()
.setCategory(TAG)
.setAction("start")
.setLabel(getVersionName)
.build())
ShadowsocksApplication.track(TAG, "start")
changeState(State.CONNECTING)
Future {
if (config.proxy == "198.199.101.152") {
val holder = getApplication.asInstanceOf[ShadowsocksApplication].containerHolder
val holder = ShadowsocksApplication.containerHolder
try {
config = ConfigUtils.getPublicConfig(getBaseContext, holder.getContainer, config)
} catch {
......@@ -371,8 +358,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val ipv6 = if (config.isIpv6) "" else "reject = ::/0;"
val conf = {
if (config.route == Route.BYPASS_CHN) {
val reject = ConfigUtils.getRejectList(getContext, application)
val blackList = ConfigUtils.getBlackList(getContext, application)
val reject = ConfigUtils.getRejectList(getContext)
val blackList = ConfigUtils.getBlackList(getContext)
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "0.0.0.0", 8153,
Path.BASE + "pdnsd-vpn.pid", reject, blackList, 8163, ipv6)
} else {
......
/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2014 <max.c.lv@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks.database
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{BaseAdapter, TextView}
import com.github.shadowsocks.R
abstract class Item {
def id: Int
def title: String
def click: Int => Unit
def longClick: Int => Boolean
}
case class IconItem(id: Int, title: String, iconRes: Int,
click: Int => Unit, longClick: Int => Boolean = _ => false)
extends Item
case class DrawableItem(id: Int, title: String, iconRes: Drawable,
click: Int => Unit, longClick: Int => Boolean = _ => false)
extends Item
case class Category(title: String)
object MenuAdapter {
trait MenuListener {
def onActiveViewChanged(v: View, pos: Int)
}
val UNDEFINED = -1
val CATEGORY = 0
val ITEM = 1
}
class MenuAdapter(context: Context, var items: List[Any]) extends BaseAdapter {
def setListener(listener: MenuAdapter.MenuListener) {
this.listener = listener
}
def setActiveId(activeId: Int) {
this.activeId = activeId
}
@Override def getCount: Int = {
items.size
}
@Override def getItem(position: Int): Object = {
items(position).asInstanceOf[Object]
}
@Override def getItemId(position: Int): Long = {
position
}
@Override override def getItemViewType(position: Int): Int = {
getItem(position) match {
case Category => MenuAdapter.CATEGORY
case IconItem => MenuAdapter.ITEM
case DrawableItem => MenuAdapter.ITEM
case _ => MenuAdapter.UNDEFINED
}
}
@Override override def getViewTypeCount: Int = {
3
}
@Override override def isEnabled(position: Int): Boolean = {
getItemViewType(position) != MenuAdapter.UNDEFINED
}
@Override override def areAllItemsEnabled: Boolean = {
false
}
@Override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
var v: View = convertView
val item = getItem(position)
item match {
case value: Category =>
if (v == null) {
v = LayoutInflater.from(context).inflate(R.layout.menu_row_category, parent, false)
}
v.asInstanceOf[TextView].setText(value.title)
case value: Item =>
if (v == null) {
v = LayoutInflater.from(context).inflate(R.layout.menu_row_item, parent, false)
}
val tv: TextView = v.asInstanceOf[TextView]
tv.setText(value.title)
value match {
case iconItem: IconItem =>
if (iconItem.iconRes != -1) {
tv.setCompoundDrawablesWithIntrinsicBounds(iconItem.iconRes, 0, 0, 0)
}
case drawableItem: DrawableItem =>
if (drawableItem.iconRes != null) {
tv.setCompoundDrawablesWithIntrinsicBounds(drawableItem.iconRes, null, null, null)
}
}
tv.setOnClickListener((view: View) => {
val item = view.getTag(R.id.mdItem).asInstanceOf[Item]
item.click(item.id)
})
tv.setOnLongClickListener((view: View) => {
val item = view.getTag(R.id.mdItem).asInstanceOf[Item]
item.longClick(item.id)
})
v.setTag(R.id.mdItem, value)
if (value.id == activeId) {
if (listener != null) listener.onActiveViewChanged(v, position)
}
case _ =>
}
v.setTag(R.id.mdActiveViewPosition, position)
v
}
def updateList(list: List[Any], activeId: Int) {
items = list
this.activeId = activeId
notifyDataSetChanged()
}
private var listener: MenuAdapter.MenuListener = null
private var activeId: Int = -1
}
package com.github.shadowsocks.preferences
import android.content.Context
import android.content.res.TypedArray
import android.preference.SwitchPreference
import android.util.AttributeSet
import com.github.shadowsocks.{R, ShadowsocksApplication}
/**
* TODO: No animations?
* @author Mygod
*/
final class NatSwitchPreference(context: Context, attrs: AttributeSet) extends SwitchPreference(context, attrs) {
private var disabled = false
override def isEnabled = super.isEnabled && !disabled
override def onGetDefaultValue(a: TypedArray, index: Int) = {
val result = ShadowsocksApplication.isVpnEnabled
if (!ShadowsocksApplication.isRoot) {
disabled = true
setEnabled(false)
setSummary(R.string.nat_summary_no_root)
}
result.asInstanceOf[AnyRef]
}
}
/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2013 <max.c.lv@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks.preferences
import android.content.{Intent, Context}
import android.preference.EditTextPreference
import android.util.AttributeSet
import com.github.shadowsocks.utils.Action
class ProfileEditTextPreference(context: Context, attrs: AttributeSet, defStyle: Int)
extends EditTextPreference(context, attrs, defStyle) {
def this(context: Context, attrs: AttributeSet) = {
this(context, attrs, android.R.attr.editTextPreferenceStyle)
mDefaultSummary = getSummary
}
override def setText(text: String) {
super.setText(text)
setSummary(text)
}
override def setSummary(summary: CharSequence) {
if (summary.toString.isEmpty) {
super.setSummary(mDefaultSummary)
} else {
super.setSummary(summary)
}
context.sendBroadcast(new Intent(Action.UPDATE_PREFS))
}
def resetSummary(summary: CharSequence) {
if (summary.toString.isEmpty) {
super.setSummary(mDefaultSummary)
} else {
super.setSummary(summary)
}
}
private var mDefaultSummary: CharSequence = getSummary
}
......@@ -198,14 +198,14 @@ object ConfigUtils {
}
def refresh(context: Context) {
val holder = context.getApplicationContext.asInstanceOf[ShadowsocksApplication].containerHolder
val holder = ShadowsocksApplication.containerHolder
if (holder != null) holder.refresh()
}
def getRejectList(context: Context, app: ShadowsocksApplication): String = {
def getRejectList(context: Context): String = {
val default = context.getString(R.string.reject)
try {
val container = app.containerHolder.getContainer
val container = ShadowsocksApplication.containerHolder.getContainer
val update = container.getString("reject")
if (update == null || update.isEmpty) default else update
} catch {
......@@ -213,10 +213,10 @@ object ConfigUtils {
}
}
def getBlackList(context: Context, app: ShadowsocksApplication): String = {
def getBlackList(context: Context): String = {
val default = context.getString(R.string.black_list)
try {
val container = app.containerHolder.getContainer
val container = ShadowsocksApplication.containerHolder.getContainer
val update = container.getString("black_list")
if (update == null || update.isEmpty) default else update
} catch {
......
......@@ -66,7 +66,6 @@ object Key {
val proxied = "Proxyed"
val isNAT = "isNAT"
val status = "status"
val proxyedApps = "proxyedApps"
val route = "route"
......@@ -111,8 +110,6 @@ object State {
object Action {
val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE"
val UPDATE_FRAGMENT = "com.github.shadowsocks.ACTION_UPDATE_FRAGMENT"
val UPDATE_PREFS = "com.github.shadowsocks.ACTION_UPDATE_PREFS"
}
object Route {
......
......@@ -42,6 +42,7 @@ import java.io._
import java.net._
import java.security.MessageDigest
import android.animation.{AnimatorListenerAdapter, Animator}
import android.app.ActivityManager
import android.content.pm.{ApplicationInfo, PackageManager}
import android.content.{Context, Intent}
......@@ -92,6 +93,9 @@ object Utils {
new String(Base64.encode(mdg.digest, 0))
}
def dpToPx(context: Context, dp: Int): Int =
Math.round(dp * (context.getResources.getDisplayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT))
/*
* round or floor depending on whether you are using offsets(floor) or
* widths(round)
......@@ -139,6 +143,16 @@ object Utils {
toast
}
def crossFade(context: Context, from: View, to: View) {
def shortAnimTime = context.getResources.getInteger(android.R.integer.config_shortAnimTime)
to.setAlpha(0)
to.setVisibility(View.VISIBLE)
to.animate().alpha(1).setDuration(shortAnimTime)
from.animate().alpha(0).setDuration(shortAnimTime).setListener(new AnimatorListenerAdapter {
override def onAnimationEnd(animation: Animator) = from.setVisibility(View.GONE)
})
}
// Blocked > 3 seconds
def toggleAirplaneMode(context: Context) {
if (Build.VERSION.SDK_INT >= 17) {
......
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