@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
|
||||
<item>
|
||||
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/material_primary_100"/>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/material_primary_100"/>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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">
|
||||
<Spinner android:id="@+id/template_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:entries="@array/acl_rule_templates"/>
|
||||
<EditText android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"/>
|
||||
</LinearLayout>
|
||||
@@ -47,6 +47,7 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:id="@+id/applistview"
|
||||
android:scrollbars="vertical"
|
||||
android:visibility="gone"/>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
android:orientation="vertical">
|
||||
<include layout="@layout/toolbar_light_dark"/>
|
||||
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/profilesList"
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingBottom="32dp"
|
||||
android:scrollbars="vertical"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"/>
|
||||
</LinearLayout>
|
||||
@@ -15,7 +15,8 @@
|
||||
android:textSize="18sp"
|
||||
android:padding="16dp"/>
|
||||
</FrameLayout>
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/selection"
|
||||
android:title="@string/action_selection"
|
||||
android:icon="@drawable/abc_ic_menu_selectall_mtrl_alpha"
|
||||
android:alphabeticShortcut="s"
|
||||
android:numericShortcut="0"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item android:id="@+id/action_select_all"
|
||||
android:title="@android:string/selectAll"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_cut"
|
||||
android:title="@android:string/cut"
|
||||
android:alphabeticShortcut="t"
|
||||
android:numericShortcut="2"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_copy"
|
||||
android:title="@android:string/copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:numericShortcut="3"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_delete"
|
||||
android:title="@string/delete"
|
||||
android:alphabeticShortcut="d"
|
||||
android:numericShortcut="4"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/action_add_rule"
|
||||
android:icon="@drawable/ic_av_playlist_add"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item android:id="@+id/action_manual_settings"
|
||||
android:title="@string/add_profile_methods_manual_settings"
|
||||
android:alphabeticShortcut="m"
|
||||
android:numericShortcut="1"/>
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import"
|
||||
android:alphabeticShortcut="i"
|
||||
android:numericShortcut="2"/>
|
||||
<item android:id="@+id/action_import_gfwlist"
|
||||
android:title="@string/route_entry_gfwlist"
|
||||
android:alphabeticShortcut="g"
|
||||
android:numericShortcut="3"/>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
@@ -88,13 +88,15 @@
|
||||
<string name="add_profile_methods_scan_qr_code">QRコードを読み取る</string>
|
||||
<string name="add_profile_methods_manual_settings">手動でセットアップ</string>
|
||||
|
||||
<string name="route_entry_gfwlist">中国本土からアクセス不可なアドレス以外を迂回する</string>
|
||||
<string-array name="route_entry">
|
||||
<item>すべて</item>
|
||||
<item>LANアドレスを迂回する</item>
|
||||
<item>中国本土のアドレスを迂回する</item>
|
||||
<item>LAN及び中国本土のアドレスを迂回する</item>
|
||||
<item>中国本土からアクセス不可なアドレス以外を迂回する</item>
|
||||
<item>@string/route_entry_gfwlist</item>
|
||||
<item>中国本土のアドレス以外を迂回する</item>
|
||||
<item>@string/custom_rules</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_apply_all">設定をすべてのプロファイルに適応する</string>
|
||||
|
||||
@@ -41,13 +41,15 @@
|
||||
<string name="auto_set_gfwlist">CHN Маршрут</string>
|
||||
<string name="auto_set_gfwlist_summary">Игнорировать все сайты находящиеся в Китае</string>
|
||||
<string name="route_list">Маршрут</string>
|
||||
<string name="route_entry_gfwlist">Список GFW</string>
|
||||
<string-array name="route_entry">
|
||||
<item>Все</item>
|
||||
<item>Обход LAN</item>
|
||||
<item>Обходной Китай</item>
|
||||
<item>Обход LAN и Китай</item>
|
||||
<item>Список GFW</item>
|
||||
<item>@string/route_entry_gfwlist</item>
|
||||
<item>Список Китай</item>
|
||||
<item>@string/custom_rules</item>
|
||||
</string-array>
|
||||
<string name="proxied_apps">Прокси для выбранных приложений</string>
|
||||
<string name="proxied_apps_summary">Установить прокси для выбранных приложений, требует режим NAT под Android 4.x</string>
|
||||
|
||||
@@ -100,13 +100,15 @@
|
||||
<string name="add_profile_methods_manual_settings">手动设置</string>
|
||||
<string name="add_profile_scanner_not_installed">请安装任意兼容 ZXing 的二维码扫描应用。</string>
|
||||
|
||||
<string name="route_entry_gfwlist">仅代理中国大陆无法访问的地址</string>
|
||||
<string-array name="route_entry">
|
||||
<item>全局</item>
|
||||
<item>绕过局域网地址</item>
|
||||
<item>绕过中国大陆地址</item>
|
||||
<item>绕过局域网及中国大陆地址</item>
|
||||
<item>仅代理中国大陆无法访问的地址</item>
|
||||
<item>@string/route_entry_gfwlist</item>
|
||||
<item>仅代理中国大陆地址</item>
|
||||
<item>@string/custom_rules</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_apply_all">应用设置到所有配置文件</string>
|
||||
@@ -142,4 +144,14 @@
|
||||
<string name="kcp_port_summary">%d(远端 KCPTUN 服务器的端口号)</string>
|
||||
<string name="kcpcli">KCP 参数</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">自定义规则</string>
|
||||
<string name="action_selection">选择…</string>
|
||||
<string name="action_add_rule">添加规则…</string>
|
||||
<string-array name="acl_rule_templates">
|
||||
<item>子网/域名 PCRE 正则表达式</item>
|
||||
<item>域名及其子域名</item>
|
||||
</string-array>
|
||||
<string name="edit_rule">编辑规则</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -101,13 +101,15 @@
|
||||
<string name="add_profile_methods_manual_settings">手動設定</string>
|
||||
<string name="add_profile_scanner_not_installed">請安裝任何兼容 ZXing 的 QR 碼掃描應用程式。</string>
|
||||
|
||||
<string name="route_entry_gfwlist">GFW List</string>
|
||||
<string-array name="route_entry">
|
||||
<item>全域</item>
|
||||
<item>略過區域網路位址</item>
|
||||
<item>略過中國大陸位址</item>
|
||||
<item>略過區域網路及中國大陸位址</item>
|
||||
<item>僅代理大陸屏蔽位址</item>
|
||||
<item>@string/route_entry_gfwlist</item>
|
||||
<item>僅代理大陸位址</item>
|
||||
<item>@string/custom_rules</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_apply_all">應用設置到所有配置文件</string>
|
||||
@@ -143,4 +145,14 @@
|
||||
<string name="kcp_port_summary">%d(遠端 KCPTUN 伺服器的連接埠號碼)</string>
|
||||
<string name="kcpcli">KCP 參數</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">自定義規則</string>
|
||||
<string name="action_selection">選擇…</string>
|
||||
<string name="action_add_rule">添加規則…</string>
|
||||
<string-array name="acl_rule_templates">
|
||||
<item>子網/主機名 PCRE 正則表達式</item>
|
||||
<item>域名及其所有子域名</item>
|
||||
</string-array>
|
||||
<string name="edit_rule">編輯規則</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<item>bypass-lan-china</item>
|
||||
<item>gfwlist</item>
|
||||
<item>china-list</item>
|
||||
<item>custom-rules</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="bypass_private_route">
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
<color name="material_green_700">#388E3C</color>
|
||||
<color name="material_green_a700">#00C853</color>
|
||||
<color name="material_blue_grey_100">#CFD8DC</color>
|
||||
<color name="material_blue_grey_300">#90A4AE</color>
|
||||
<color name="material_blue_grey_500">#607D8B</color>
|
||||
<color name="material_blue_grey_700">#455A64</color>
|
||||
<color name="material_primary_100">@color/material_blue_grey_100</color>
|
||||
<color name="material_primary_300">@color/material_blue_grey_300</color>
|
||||
<color name="material_primary_500">@color/material_blue_grey_500</color>
|
||||
<color name="material_primary_700">@color/material_blue_grey_700</color>
|
||||
|
||||
@@ -48,13 +48,15 @@
|
||||
<string name="auto_set_gfwlist_summary">Bypass all sites located in China
|
||||
</string>
|
||||
<string name="route_list">Route</string>
|
||||
<string name="route_entry_gfwlist">GFW List</string>
|
||||
<string-array name="route_entry">
|
||||
<item>All</item>
|
||||
<item>Bypass LAN</item>
|
||||
<item>Bypass China</item>
|
||||
<item>Bypass LAN & China</item>
|
||||
<item>GFW List</item>
|
||||
<item>@string/route_entry_gfwlist</item>
|
||||
<item>China List</item>
|
||||
<item>@string/custom_rules</item>
|
||||
</string-array>
|
||||
<string name="proxied_apps">Per-App Proxy</string>
|
||||
<string name="proxied_apps_summary">Set proxy for selected apps, requires NAT mode under Android 4.x</string>
|
||||
@@ -156,4 +158,14 @@
|
||||
<string name="kcp_port">KCP Port</string>
|
||||
<string name="kcp_port_summary">%d (port number of the remote KCPTUN server)</string>
|
||||
<string name="kcpcli">KCP Parameters (CLI)</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">Custom rules</string>
|
||||
<string name="action_selection">Selection…</string>
|
||||
<string name="action_add_rule">Add rule(s)…</string>
|
||||
<string-array name="acl_rule_templates">
|
||||
<item>Subnet/Hostname PCRE pattern</item>
|
||||
<item>Domain name and all its subdomain names</item>
|
||||
</string-array>
|
||||
<string name="edit_rule">Edit rule</string>
|
||||
</resources>
|
||||
|
||||
@@ -221,7 +221,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
initProxiedApps()
|
||||
loadingView = findViewById(R.id.loading)
|
||||
appListView = findViewById(R.id.applistview).asInstanceOf[RecyclerView]
|
||||
appListView.setLayoutManager(new LinearLayoutManager(this))
|
||||
appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false))
|
||||
appListView.setItemAnimator(new DefaultItemAnimator)
|
||||
|
||||
instance = this
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils._
|
||||
@@ -132,38 +133,39 @@ trait BaseService extends Service {
|
||||
false
|
||||
} else true
|
||||
|
||||
def connect(): Unit = if (profile.host == "198.199.101.152") {
|
||||
val holder = app.containerHolder
|
||||
val container = holder.getContainer
|
||||
val url = container.getString("proxy_url")
|
||||
val sig = Utils.getSignature(this)
|
||||
def connect() {
|
||||
if (profile.host == "198.199.101.152") {
|
||||
val holder = app.containerHolder
|
||||
val container = holder.getContainer
|
||||
val url = container.getString("proxy_url")
|
||||
val sig = Utils.getSignature(this)
|
||||
|
||||
val client = new OkHttpClient.Builder()
|
||||
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
|
||||
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
|
||||
case _ => Dns.SYSTEM.lookup(hostname)
|
||||
})
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
val requestBody = new FormBody.Builder()
|
||||
.add("sig", sig)
|
||||
.build()
|
||||
val request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
val client = new OkHttpClient.Builder()
|
||||
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
|
||||
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
|
||||
case _ => Dns.SYSTEM.lookup(hostname)
|
||||
})
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
val requestBody = new FormBody.Builder()
|
||||
.add("sig", sig)
|
||||
.build()
|
||||
val request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val resposne = client.newCall(request).execute()
|
||||
val list = resposne.body.string
|
||||
|
||||
val proxies = Random.shuffle(list.split('|').toSeq)
|
||||
val proxy = proxies.head.split(':')
|
||||
profile.host = proxy(0).trim
|
||||
profile.remotePort = proxy(1).trim.toInt
|
||||
profile.password = proxy(2).trim
|
||||
profile.method = proxy(3).trim
|
||||
val proxies = Random.shuffle(client.newCall(request).execute().body.string.split('|').toSeq)
|
||||
val proxy = proxies.head.split(':')
|
||||
profile.host = proxy(0).trim
|
||||
profile.remotePort = proxy(1).trim.toInt
|
||||
profile.password = proxy(2).trim
|
||||
profile.method = proxy(3).trim
|
||||
}
|
||||
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
|
||||
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
|
||||
}
|
||||
|
||||
def startRunner(profile: Profile) {
|
||||
|
||||
@@ -43,6 +43,7 @@ import android.view.View
|
||||
import android.widget.{TextView, Toast}
|
||||
import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.CustomRulesFragment
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
|
||||
import com.github.shadowsocks.utils._
|
||||
@@ -62,6 +63,7 @@ object MainActivity {
|
||||
private final val DRAWER_RECOVERY = 2L
|
||||
private final val DRAWER_ABOUT = 3L
|
||||
private final val DRAWER_FAQ = 4L
|
||||
private final val DRAWER_CUSTOM_RULES = 5L
|
||||
}
|
||||
|
||||
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
|
||||
@@ -84,6 +86,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
|
||||
private var currentFragment: ToolbarFragment = _
|
||||
private lazy val profilesFragment = new ProfilesFragment()
|
||||
private lazy val customRulesFragment = new CustomRulesFragment()
|
||||
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
|
||||
private lazy val aboutFragment = new AboutFragment()
|
||||
private lazy val customTabsIntent = new CustomTabsIntent.Builder()
|
||||
@@ -193,6 +196,11 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
.withName(R.string.profiles)
|
||||
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
|
||||
.withIconTintingEnabled(true),
|
||||
new PrimaryDrawerItem()
|
||||
.withIdentifier(DRAWER_CUSTOM_RULES)
|
||||
.withName(R.string.custom_rules)
|
||||
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_assignment))
|
||||
.withIconTintingEnabled(true),
|
||||
new PrimaryDrawerItem()
|
||||
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
|
||||
.withName(R.string.settings)
|
||||
@@ -377,6 +385,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
app.track(TAG, "about")
|
||||
displayFragment(aboutFragment)
|
||||
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
|
||||
case DRAWER_CUSTOM_RULES => displayFragment(customRulesFragment)
|
||||
}
|
||||
true // unexpected cases will throw exception
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
case _ => false
|
||||
}
|
||||
|
||||
final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
|
||||
final class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view)
|
||||
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
var item: Profile = _
|
||||
@@ -256,7 +256,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
.putExtra(Action.EXTRA_PROFILE_ID, id))
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
|
||||
inflater.inflate(R.layout.layout_profiles, container, false)
|
||||
inflater.inflate(R.layout.layout_list, container, false)
|
||||
|
||||
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -265,18 +265,19 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
|
||||
val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
val layoutManager = new LinearLayoutManager(getActivity)
|
||||
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
||||
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
|
||||
profilesList.setLayoutManager(layoutManager)
|
||||
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
||||
case (profile, i) if profile.id == app.profileId => i
|
||||
}.getOrElse(-1))
|
||||
val animator = new DefaultItemAnimator()
|
||||
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
|
||||
profilesList.setItemAnimator(animator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
instance = this
|
||||
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
||||
case (profile, i) if profile.id == app.profileId => i
|
||||
}.getOrElse(-1))
|
||||
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar), profilesAdapter.undo, profilesAdapter.commit)
|
||||
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar),
|
||||
profilesAdapter.undo, profilesAdapter.commit)
|
||||
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
||||
@@ -288,7 +289,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
def onSwiped(viewHolder: ViewHolder, direction: Int) {
|
||||
val index = viewHolder.getAdapterPosition
|
||||
profilesAdapter.remove(index)
|
||||
undoManager.remove(index, viewHolder.asInstanceOf[ProfileViewHolder].item)
|
||||
undoManager.remove((index, viewHolder.asInstanceOf[ProfileViewHolder].item))
|
||||
}
|
||||
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = {
|
||||
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
|
||||
|
||||
@@ -30,7 +30,18 @@ import android.widget.{ImageView, LinearLayout}
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
final class QRCodeDialog(url: String) extends DialogFragment {
|
||||
object QRCodeDialog {
|
||||
private final val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
|
||||
}
|
||||
|
||||
final class QRCodeDialog extends DialogFragment {
|
||||
import QRCodeDialog._
|
||||
def this(url: String) {
|
||||
this()
|
||||
getArguments.putString(KEY_URL, url)
|
||||
}
|
||||
private def url = getArguments.getString(KEY_URL)
|
||||
|
||||
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
|
||||
private var adapter: NfcAdapter = _
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.github.shadowsocks.acl.DonaldTrump
|
||||
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
|
||||
import com.github.shadowsocks.job.DonaldTrump
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
|
||||
|
||||
@@ -28,8 +28,8 @@ import android.content._
|
||||
import android.os._
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.job.AclSyncJob
|
||||
import com.github.shadowsocks.utils._
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
|
||||
@@ -76,9 +76,9 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
|
||||
|
||||
if (profile.route != Route.ALL) {
|
||||
if (profile.route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
|
||||
cmd += Acl.getPath(profile.route)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -169,11 +169,11 @@ class ShadowsocksNatService extends BaseService {
|
||||
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
|
||||
|
||||
val conf = profile.route match {
|
||||
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
|
||||
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
|
||||
getBlackList, reject, profile.localPort + 63, reject)
|
||||
case Route.CHINALIST =>
|
||||
case Acl.CHINALIST =>
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
|
||||
"", reject, profile.localPort + 63, reject)
|
||||
@@ -311,7 +311,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
handleConnection()
|
||||
|
||||
if (profile.route != Route.ALL)
|
||||
if (profile.route != Acl.ALL)
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
changeState(State.CONNECTED)
|
||||
|
||||
@@ -30,8 +30,8 @@ import android.net.VpnService
|
||||
import android.os._
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.job.AclSyncJob
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
@@ -145,7 +145,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
handleConnection()
|
||||
changeState(State.CONNECTED)
|
||||
|
||||
if (profile.route != Route.ALL)
|
||||
if (profile.route != Acl.ALL)
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
notification = new ShadowsocksNotification(this, profile.getName)
|
||||
@@ -241,9 +241,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (profile.udpdns && !profile.kcp) cmd += "-u"
|
||||
|
||||
if (profile.route != Route.ALL) {
|
||||
if (profile.route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
|
||||
cmd += Acl.getPath(profile.route)
|
||||
}
|
||||
|
||||
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
|
||||
@@ -291,11 +291,11 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
|
||||
val protect = "protect = \"" + protectPath +"\";"
|
||||
val conf = profile.route match {
|
||||
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
|
||||
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
|
||||
getBlackList, reject, profile.localPort + 63, reject)
|
||||
case Route.CHINALIST =>
|
||||
case Acl.CHINALIST =>
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
|
||||
"", reject, profile.localPort + 63, reject)
|
||||
@@ -347,7 +347,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.route == Route.ALL || profile.route == Route.BYPASS_CHN) {
|
||||
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_CHN) {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
} else {
|
||||
val privateList = getResources.getStringArray(R.array.bypass_private_route)
|
||||
|
||||
@@ -35,7 +35,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
* @author CzBiX
|
||||
*/
|
||||
class TaskerActivity extends AppCompatActivity {
|
||||
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
private class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
{
|
||||
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
|
||||
view.setBackgroundResource(typedArray.getResourceId(0, 0))
|
||||
@@ -92,8 +92,8 @@ class TaskerActivity extends AppCompatActivity {
|
||||
taskerOption = TaskerSettings.fromIntent(getIntent)
|
||||
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
|
||||
switch.setChecked(taskerOption.switchOn)
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
val lm = new LinearLayoutManager(this)
|
||||
val profilesList = findViewById(R.id.list).asInstanceOf[RecyclerView]
|
||||
val lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
profilesList.setLayoutManager(lm)
|
||||
profilesList.setItemAnimator(new DefaultItemAnimator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.IOUtils
|
||||
import com.j256.ormlite.field.DatabaseField
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.io.Source
|
||||
|
||||
/**
|
||||
* ACL handler compliant with: src/main/jni/shadowsocks-libev/src/acl.c
|
||||
*
|
||||
* OrmLite integration is unused for now.
|
||||
*
|
||||
* @author Mygod
|
||||
*/
|
||||
class Acl {
|
||||
@DatabaseField(generatedId = true)
|
||||
var id: Int = _
|
||||
val bypassHostnames = new mutable.SortedList[String]()
|
||||
val proxyHostnames = new mutable.SortedList[String]()
|
||||
val subnets = new mutable.SortedList[Subnet]()
|
||||
@DatabaseField
|
||||
var bypass: Boolean = _
|
||||
|
||||
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
|
||||
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
|
||||
def getSubnetsString: String = subnets.mkString("\n")
|
||||
def setBypassHostnamesString(value: String) {
|
||||
bypassHostnames.clear()
|
||||
bypassHostnames ++= value.split("\n")
|
||||
}
|
||||
def setProxyHostnamesString(value: String) {
|
||||
proxyHostnames.clear()
|
||||
proxyHostnames ++= value.split("\n")
|
||||
}
|
||||
def setSubnetsString(value: String) {
|
||||
subnets.clear()
|
||||
subnets ++= value.split("\n").map(Subnet.fromString)
|
||||
}
|
||||
|
||||
def fromAcl(other: Acl): Acl = {
|
||||
bypassHostnames.clear()
|
||||
bypassHostnames ++= other.bypassHostnames
|
||||
proxyHostnames.clear()
|
||||
proxyHostnames ++= other.proxyHostnames
|
||||
subnets.clear()
|
||||
subnets ++= other.subnets
|
||||
bypass = other.bypass
|
||||
this
|
||||
}
|
||||
def fromSource(value: Source): Acl = {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
this.subnets.clear()
|
||||
bypass = false
|
||||
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
|
||||
lazy val proxySubnets = new mutable.SortedList[Subnet]()
|
||||
var hostnames: mutable.SortedList[String] = proxyHostnames
|
||||
var subnets: mutable.SortedList[Subnet] = proxySubnets
|
||||
for (line <- value.getLines()) (line.indexOf('#') match {
|
||||
case -1 => line
|
||||
case index => line.substring(0, index) // trim comments
|
||||
}).trim match {
|
||||
case "[outbound_block_list]" =>
|
||||
hostnames = null
|
||||
subnets = null
|
||||
case "[black_list]" | "[bypass_list]" =>
|
||||
hostnames = bypassHostnames
|
||||
subnets = bypassSubnets
|
||||
case "[white_list]" | "[proxy_list]" =>
|
||||
hostnames = proxyHostnames
|
||||
subnets = proxySubnets
|
||||
case "[reject_all]" | "[bypass_all]" => bypass = true
|
||||
case "[accept_all]" | "[proxy_all]" => bypass = false
|
||||
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
|
||||
case _: IllegalArgumentException => hostnames += input
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
this.subnets ++= (if (bypass) bypassSubnets else proxySubnets)
|
||||
this
|
||||
}
|
||||
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getPath(id)))
|
||||
|
||||
override def toString: String = {
|
||||
val result = new StringBuilder()
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
var bypassList = bypassHostnames.toStream
|
||||
var proxyList = proxyHostnames.toStream
|
||||
if (bypass) bypassList = subnets.toStream.map(_.toString) #::: bypassList
|
||||
else proxyList = subnets.toStream.map(_.toString) #::: proxyList
|
||||
if (bypassList.nonEmpty) {
|
||||
result.append("[bypass_list]\n")
|
||||
result.append(bypassList.mkString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
if (proxyList.nonEmpty) {
|
||||
result.append("[proxy_list]\n")
|
||||
result.append(proxyList.mkString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
result.toString
|
||||
}
|
||||
|
||||
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
|
||||
|
||||
// Don't change: dummy fields for OrmLite interaction
|
||||
|
||||
// noinspection ScalaUnusedSymbol
|
||||
@DatabaseField(useGetSet = true)
|
||||
private val bypassHostnamesString: String = null
|
||||
// noinspection ScalaUnusedSymbol
|
||||
@DatabaseField(useGetSet = true)
|
||||
private val proxyHostnamesString: String = null
|
||||
// noinspection ScalaUnusedSymbol
|
||||
@DatabaseField(useGetSet = true)
|
||||
private val subnetsString: String = null
|
||||
}
|
||||
|
||||
object Acl {
|
||||
final val ALL = "all"
|
||||
final val BYPASS_LAN = "bypass-lan"
|
||||
final val BYPASS_CHN = "bypass-china"
|
||||
final val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
final val GFWLIST = "gfwlist"
|
||||
final val CHINALIST = "china-list"
|
||||
final val CUSTOM_RULES = "custom-rules"
|
||||
|
||||
def getPath(id: String): String = app.getApplicationInfo.dataDir + '/' + id + ".acl"
|
||||
def customRules: Acl = {
|
||||
val acl = new Acl()
|
||||
try acl.fromId(CUSTOM_RULES) catch {
|
||||
case _: FileNotFoundException =>
|
||||
}
|
||||
acl.bypass = true
|
||||
acl.bypassHostnames.clear() // everything is bypassed
|
||||
acl
|
||||
}
|
||||
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getPath(id), acl.toString)
|
||||
}
|
||||
+5
-6
@@ -18,18 +18,18 @@
|
||||
/* */
|
||||
/*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.job
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.evernote.android.job.Job.{Params, Result}
|
||||
import com.evernote.android.job.{Job, JobRequest}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils.IOUtils
|
||||
|
||||
import scala.io.Source
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
@@ -50,9 +50,8 @@ class AclSyncJob(route: String) extends Job {
|
||||
val filename = route + ".acl"
|
||||
try {
|
||||
//noinspection JavaAccessorMethodCalledAsEmptyParen
|
||||
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename, autoClose(
|
||||
new URL("https://shadowsocks.org/acl/android/v1/" +
|
||||
filename).openConnection().getInputStream())(IOUtils.readString))
|
||||
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename,
|
||||
Source.fromURL("https://shadowsocks.org/acl/android/v1/" + filename).mkString)
|
||||
Result.SUCCESS
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import java.net.IDN
|
||||
import java.util.Locale
|
||||
|
||||
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||
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._
|
||||
import android.widget.{EditText, Spinner, TextView, Toast}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.State
|
||||
import com.github.shadowsocks.widget.UndoSnackbarManager
|
||||
import com.github.shadowsocks.{MainActivity, R, ToolbarFragment}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object CustomRulesFragment {
|
||||
private final val GENERIC = 0
|
||||
private final val DOMAIN = 1
|
||||
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
|
||||
private final val TEMPLATE_DOMAIN = "(^|\\.)%s$"
|
||||
|
||||
private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
|
||||
private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
|
||||
}
|
||||
|
||||
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
|
||||
import CustomRulesFragment._
|
||||
|
||||
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
|
||||
case State.CONNECTED => app.currentProfile.get.route != Acl.CUSTOM_RULES
|
||||
case State.STOPPED => true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private def createAclRuleDialog(text: CharSequence = "") = {
|
||||
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
|
||||
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
|
||||
val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
|
||||
PATTERN_DOMAIN.findFirstMatchIn(text) match {
|
||||
case Some(m) =>
|
||||
templateSelector.setSelection(DOMAIN)
|
||||
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
|
||||
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
|
||||
case None =>
|
||||
templateSelector.setSelection(GENERIC)
|
||||
editText.setText(text)
|
||||
}
|
||||
(templateSelector, editText, new AlertDialog.Builder(getActivity)
|
||||
.setTitle(R.string.edit_rule)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(view))
|
||||
}
|
||||
|
||||
private val handler = new Handler()
|
||||
private val selectedItems = new mutable.HashSet[AnyRef]
|
||||
private def onSelectedItemsUpdated(): Unit = selectionItem.setVisible(selectedItems.nonEmpty)
|
||||
|
||||
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
|
||||
with View.OnClickListener with View.OnLongClickListener {
|
||||
var item: AnyRef = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnLongClickListener(this)
|
||||
itemView.setBackgroundResource(R.drawable.background_selectable)
|
||||
|
||||
def bind(hostname: String) {
|
||||
item = hostname
|
||||
text.setText(hostname)
|
||||
itemView.setSelected(selectedItems.contains(hostname))
|
||||
}
|
||||
def bind(subnet: Subnet) {
|
||||
item = subnet
|
||||
text.setText(subnet.toString)
|
||||
itemView.setSelected(selectedItems.contains(subnet))
|
||||
}
|
||||
|
||||
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
|
||||
val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
|
||||
dialog
|
||||
.setNeutralButton(R.string.delete, ((_, _) => {
|
||||
adapter.remove(item)
|
||||
undoManager.remove((-1, item))
|
||||
}): DialogInterface.OnClickListener)
|
||||
.setPositiveButton(android.R.string.ok, ((_, _) =>
|
||||
if (adapter.addFromTemplate(templateSelector.getSelectedItemPosition, editText.getText)) adapter.remove(item))
|
||||
: DialogInterface.OnClickListener)
|
||||
.create().show()
|
||||
}
|
||||
def onLongClick(v: View): Boolean = {
|
||||
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
|
||||
onSelectedItemsUpdated()
|
||||
itemView.setSelected(!itemView.isSelected)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private final class AclRulesAdapter extends RecyclerView.Adapter[AclRuleViewHolder] {
|
||||
private val acl = Acl.customRules
|
||||
private var savePending: Boolean = _
|
||||
private def apply() = if (!savePending) {
|
||||
savePending = true
|
||||
handler.post(() => {
|
||||
Acl.save(Acl.CUSTOM_RULES, acl)
|
||||
savePending = false
|
||||
})
|
||||
}
|
||||
|
||||
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
|
||||
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
|
||||
val j = i - acl.subnets.size
|
||||
if (j < 0) vh.bind(acl.subnets(i)) else vh.bind(acl.proxyHostnames(j))
|
||||
}
|
||||
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
|
||||
.inflate(android.R.layout.simple_list_item_1, vg, false))
|
||||
|
||||
def addSubnet(subnet: Subnet): Boolean = if (acl.subnets.add(subnet)) {
|
||||
notifyItemInserted(acl.subnets.indexOf(subnet))
|
||||
apply()
|
||||
true
|
||||
} else false
|
||||
def addHostname(hostname: String): Boolean = if (acl.proxyHostnames.add(hostname)) {
|
||||
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
|
||||
apply()
|
||||
true
|
||||
} else false
|
||||
def addToProxy(input: String): Boolean = try addSubnet(Subnet.fromString(input)) catch {
|
||||
case _: IllegalArgumentException => addHostname(input)
|
||||
}
|
||||
def addFromTemplate(template: Int, text: CharSequence): Boolean = template match {
|
||||
case GENERIC => addToProxy(text.toString)
|
||||
case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
|
||||
IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
|
||||
case exc: IllegalArgumentException =>
|
||||
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def remove(i: Int) {
|
||||
val j = i - acl.subnets.size
|
||||
if (j < 0) acl.subnets.remove(i) else acl.proxyHostnames.remove(j)
|
||||
notifyItemRemoved(i)
|
||||
apply()
|
||||
}
|
||||
def remove(item: AnyRef): Unit = item match {
|
||||
case subnet: Subnet =>
|
||||
notifyItemRemoved(acl.subnets.indexOf(subnet))
|
||||
acl.subnets.remove(subnet)
|
||||
apply()
|
||||
case hostname: String =>
|
||||
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
|
||||
acl.proxyHostnames.remove(hostname)
|
||||
apply()
|
||||
}
|
||||
def removeSelected() {
|
||||
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
|
||||
selectedItems.foreach(remove)
|
||||
selectedItems.clear()
|
||||
onSelectedItemsUpdated()
|
||||
}
|
||||
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
|
||||
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
|
||||
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
|
||||
apply()
|
||||
}
|
||||
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
|
||||
notifyItemInserted(acl.subnets.indexOf(subnet))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
def selectAll() {
|
||||
selectedItems.clear()
|
||||
selectedItems ++= acl.subnets
|
||||
selectedItems ++= acl.proxyHostnames
|
||||
onSelectedItemsUpdated()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy val adapter = new AclRulesAdapter()
|
||||
private var list: RecyclerView = _
|
||||
private var selectionItem: MenuItem = _
|
||||
private var undoManager: UndoSnackbarManager[AnyRef] = _
|
||||
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
|
||||
inflater.inflate(R.layout.layout_list, container, false)
|
||||
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
savedInstanceState.getStringArray(SELECTED_SUBNETS) match {
|
||||
case null =>
|
||||
case arr => selectedItems ++= arr.map(Subnet.fromString)
|
||||
}
|
||||
savedInstanceState.getStringArray(SELECTED_HOSTNAMES) match {
|
||||
case null =>
|
||||
case arr => selectedItems ++= arr
|
||||
}
|
||||
onSelectedItemsUpdated()
|
||||
}
|
||||
toolbar.setTitle(R.string.custom_rules)
|
||||
toolbar.inflateMenu(R.menu.custom_rules_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
selectionItem = toolbar.getMenu.findItem(R.id.selection)
|
||||
app.updateAssets()
|
||||
list = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
|
||||
list.setItemAnimator(new DefaultItemAnimator)
|
||||
list.setAdapter(adapter)
|
||||
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
|
||||
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
||||
if (isEnabled) super.getSwipeDirs(recyclerView, viewHolder) else 0
|
||||
def onSwiped(viewHolder: ViewHolder, direction: Int): Unit = adapter.remove(viewHolder.getAdapterPosition)
|
||||
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = false
|
||||
}).attachToRecyclerView(list)
|
||||
}
|
||||
|
||||
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
|
||||
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
|
||||
}
|
||||
|
||||
override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
|
||||
case R.id.action_select_all =>
|
||||
adapter.selectAll()
|
||||
true
|
||||
case R.id.action_cut =>
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
|
||||
adapter.removeSelected()
|
||||
true
|
||||
case R.id.action_copy =>
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
|
||||
true
|
||||
case R.id.action_delete =>
|
||||
adapter.removeSelected()
|
||||
true
|
||||
|
||||
case R.id.action_manual_settings =>
|
||||
val (templateSelector, editText, dialog) = createAclRuleDialog()
|
||||
dialog
|
||||
.setPositiveButton(android.R.string.ok, ((_, _) => adapter.addFromTemplate(
|
||||
templateSelector.getSelectedItemPosition, editText.getText)): DialogInterface.OnClickListener)
|
||||
.create().show()
|
||||
true
|
||||
case R.id.action_import =>
|
||||
clipboard.getPrimaryClip.getItemAt(0).getText.toString.split("\n").foreach(adapter.addToProxy)
|
||||
true
|
||||
case R.id.action_import_gfwlist =>
|
||||
val acl = new Acl().fromId(Acl.GFWLIST)
|
||||
acl.subnets.foreach(adapter.addSubnet)
|
||||
acl.proxyHostnames.foreach(adapter.addHostname)
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
/* */
|
||||
/*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.job
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobCreator
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import java.net.{Inet4Address, Inet6Address, InetAddress}
|
||||
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
@throws[IllegalArgumentException]
|
||||
class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] {
|
||||
if (prefixSize < 0) throw new IllegalArgumentException
|
||||
address match {
|
||||
case _: Inet4Address => if (prefixSize > 32) throw new IllegalArgumentException
|
||||
case _: Inet6Address => if (prefixSize > 128) throw new IllegalArgumentException
|
||||
}
|
||||
|
||||
override def toString: String = if (address match {
|
||||
case _: Inet4Address => prefixSize == 32
|
||||
case _: Inet6Address => prefixSize == 128
|
||||
}) address.toString else address.toString + '/' + prefixSize
|
||||
|
||||
override def compareTo(that: Subnet): Int = {
|
||||
val addrThis = address.getAddress
|
||||
val addrThat = that.address.getAddress
|
||||
var result = addrThis lengthCompare addrThat.length // IPv4 address goes first
|
||||
if (result != 0) return result
|
||||
for ((x, y) <- addrThis zip addrThat) {
|
||||
result = x compare y
|
||||
if (result != 0) return result
|
||||
}
|
||||
prefixSize compare that.prefixSize
|
||||
}
|
||||
}
|
||||
|
||||
object Subnet {
|
||||
@throws[IllegalArgumentException]
|
||||
def fromString(value: String): Subnet = {
|
||||
val parts = value.split("/")
|
||||
if (!Utils.isNumeric(parts(0))) throw new IllegalArgumentException()
|
||||
val addr = InetAddress.getByName(parts(0))
|
||||
parts.length match {
|
||||
case 1 => new Subnet(addr, addr match {
|
||||
case _: Inet4Address => 32
|
||||
case _: Inet6Address => 128
|
||||
})
|
||||
case 2 => new Subnet(addr, parts(1).toInt)
|
||||
case _ => throw new IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,12 +179,3 @@ object Action {
|
||||
|
||||
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
|
||||
}
|
||||
|
||||
object Route {
|
||||
val ALL = "all"
|
||||
val BYPASS_LAN = "bypass-lan"
|
||||
val BYPASS_CHN = "bypass-china"
|
||||
val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
val GFWLIST = "gfwlist"
|
||||
val CHINALIST = "china-list"
|
||||
}
|
||||
|
||||
@@ -37,16 +37,6 @@ object IOUtils {
|
||||
}
|
||||
}
|
||||
|
||||
def readString(in: InputStream): String = {
|
||||
val builder = new StringBuilder()
|
||||
val buffer = new Array[Byte](BUFFER_SIZE)
|
||||
while (true) {
|
||||
val count = in.read(buffer)
|
||||
if (count >= 0) builder.append(new String(buffer, 0, count)) else return builder.toString()
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
def writeString(file: String, content: String): Unit =
|
||||
autoClose(new FileWriter(file))(writer => writer.write(content))
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
|
||||
}
|
||||
private var last: Snackbar = _
|
||||
|
||||
def remove(index: Int, item: T) {
|
||||
recycleBin.append((index, item))
|
||||
def remove(items: (Int, T)*) {
|
||||
recycleBin.appendAll(items)
|
||||
val count = recycleBin.length
|
||||
last = Snackbar
|
||||
.make(view, view.getResources.getQuantityString(R.plurals.removed, count, count: Integer), Snackbar.LENGTH_LONG)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package scala.collection.mutable
|
||||
|
||||
import scala.collection.SortedSetLike
|
||||
import scala.collection.generic.MutableSortedSetFactory
|
||||
import scala.collection.immutable.RedBlackTree.Tree
|
||||
import scala.collection.immutable.{RedBlackTree => RB}
|
||||
import scala.runtime.ObjectRef
|
||||
|
||||
/**
|
||||
* @define Coll `mutable.SortedList`
|
||||
* @define coll mutable sorted list
|
||||
* @factoryInfo
|
||||
* Companion object of SortedList providing factory related utilities.
|
||||
* @author Mygod
|
||||
*/
|
||||
//noinspection ReferenceMustBePrefixed,ScalaDocUnknownTag
|
||||
object SortedList extends MutableSortedSetFactory[SortedList] {
|
||||
/**
|
||||
* The empty list of this type
|
||||
*/
|
||||
def empty[A](implicit ordering: Ordering[A]) = new SortedList[A]()
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable SortedList heavily based on SortedSet which "is not designed to enable meaningful subclassing".
|
||||
*
|
||||
* Based on: https://github.com/scala/scala/blob/3cc99d7/src/library/scala/collection/mutable/TreeSet.scala
|
||||
*
|
||||
* @author Mygod
|
||||
*/
|
||||
//noinspection ReferenceMustBePrefixed
|
||||
class SortedList[A] private(treeRef: ObjectRef[RB.Tree[A, Null]], from: Option[A], until: Option[A])
|
||||
(implicit val ordering: Ordering[A])
|
||||
extends SortedSet[A] with SetLike[A, SortedList[A]] with SortedSetLike[A, SortedList[A]] with Set[A] with Serializable {
|
||||
def insert(elem: A): Boolean = {
|
||||
val old = size
|
||||
this += elem
|
||||
old < size
|
||||
}
|
||||
|
||||
// AbstractSeq
|
||||
def apply(i: Int): A = RB.nth(treeRef.elem, i).key // Should i out of range result in NullReferenceException?
|
||||
def length: Int = size
|
||||
|
||||
// Buffer
|
||||
def +=:(elem: A): SortedList.this.type = +=(elem) // prepending is the same as the list is sorted
|
||||
def remove(n: Int): A = {
|
||||
val result = apply(n)
|
||||
remove(result)
|
||||
result
|
||||
}
|
||||
def update(idx: Int, elem: A) {
|
||||
val x = apply(idx)
|
||||
if (!ordering.equiv(x, elem)) {
|
||||
remove(x)
|
||||
this.add(elem)
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized methods
|
||||
override def clear(): Unit = treeRef.elem = null
|
||||
def indexOf[B >: A](elem: B, from: Int = 0): Int = lookupIndex(treeRef.elem, elem.asInstanceOf[A]) match {
|
||||
case result if result < from => -1
|
||||
case result => result
|
||||
}
|
||||
override def last: A = RB.greatest(treeRef.elem).key
|
||||
|
||||
// Tree helper methods, TODO: expand to non-recursive form?
|
||||
private def lookupIndex(tree: Tree[A, Null], x: A): Int =
|
||||
if (tree eq null) -1 else ordering.compare(x, tree.key) match {
|
||||
case 0 => RB.count(tree.left)
|
||||
case cmp if cmp < 0 => lookupIndex(tree.left, x)
|
||||
case _ => lookupIndex(tree.right, x) match {
|
||||
case i if i < 0 => i
|
||||
case i => RB.count(tree.left) + 1 + i
|
||||
}
|
||||
}
|
||||
|
||||
// The following is copied from TreeSet
|
||||
|
||||
if (ordering eq null)
|
||||
throw new NullPointerException("ordering must not be null")
|
||||
|
||||
def this()(implicit ordering: Ordering[A]) = this(new ObjectRef(null), None, None)
|
||||
|
||||
override def size: Int = RB.countInRange(treeRef.elem, from, until)
|
||||
|
||||
override def stringPrefix = "SortedList"
|
||||
|
||||
override def empty: SortedList[A] = SortedList.empty
|
||||
|
||||
private def pickBound(comparison: (A, A) => A, oldBound: Option[A], newBound: Option[A]) = (newBound, oldBound) match {
|
||||
case (Some(newB), Some(oldB)) => Some(comparison(newB, oldB))
|
||||
case (None, _) => oldBound
|
||||
case _ => newBound
|
||||
}
|
||||
|
||||
override def rangeImpl(fromArg: Option[A], untilArg: Option[A]): SortedList[A] = {
|
||||
val newFrom = pickBound(ordering.max, fromArg, from)
|
||||
val newUntil = pickBound(ordering.min, untilArg, until)
|
||||
|
||||
new SortedList(treeRef, newFrom, newUntil)
|
||||
}
|
||||
|
||||
override def -=(elem: A): this.type = {
|
||||
treeRef.elem = RB.delete(treeRef.elem, elem)
|
||||
this
|
||||
}
|
||||
|
||||
override def +=(elem: A): this.type = {
|
||||
treeRef.elem = RB.update(treeRef.elem, elem, null, overwrite = false)
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Thanks to the immutable nature of the
|
||||
* underlying Tree, we can share it with
|
||||
* the clone. So clone complexity in time is O(1).
|
||||
*
|
||||
*/
|
||||
override def clone(): SortedList[A] =
|
||||
new SortedList[A](new ObjectRef(treeRef.elem), from, until)
|
||||
|
||||
private val notProjection = !(from.isDefined || until.isDefined)
|
||||
|
||||
override def contains(elem: A): Boolean = {
|
||||
def leftAcceptable: Boolean = from match {
|
||||
case Some(lb) => ordering.gteq(elem, lb)
|
||||
case _ => true
|
||||
}
|
||||
|
||||
def rightAcceptable: Boolean = until match {
|
||||
case Some(ub) => ordering.lt(elem, ub)
|
||||
case _ => true
|
||||
}
|
||||
|
||||
(notProjection || (leftAcceptable && rightAcceptable)) &&
|
||||
RB.contains(treeRef.elem, elem)
|
||||
}
|
||||
|
||||
override def iterator: Iterator[A] = iteratorFrom(None)
|
||||
|
||||
override def keysIteratorFrom(start: A): Iterator[A] = iteratorFrom(Some(start))
|
||||
|
||||
private def iteratorFrom(start: Option[A]) = {
|
||||
val it = RB.keysIterator(treeRef.elem, pickBound(ordering.max, from, start))
|
||||
until match {
|
||||
case None => it
|
||||
case Some(ub) => it takeWhile (k => ordering.lt(k, ub))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user