Commit 0234acbf authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1056 from Mygod/custom-rules

Implement custom rules
parents 8374a8cd 292c76b2
<?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>
<?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>
<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>
<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>
<?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>
<?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>
......@@ -83,18 +83,19 @@
<string name="connection_test_error_status_code">ステータスコード無効(#%d)</string>
<!-- profile -->
<string name="copy_url">URL をコピー</string>
<string name="add_profile_dialog">シャドーソックスにプロファイルを追加しますか</string>
<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>
......@@ -108,7 +110,6 @@
<string name="add_profile_dialog">Добавить этот Профиль Shadowsocks?</string>
<string name="add_profile_methods_scan_qr_code">Сканировать QR код</string>
<string name="add_profile_methods_manual_settings">Настройка вручную</string>
<string name="copy_url">Копировать URL</string>
<plurals name="removed">
<item quantity="one">Удалён</item>
<item quantity="other">%d элементы удалены</item>
......
......@@ -94,19 +94,20 @@
<string name="delete">删除</string>
<string name="delete_confirm_prompt">你是否确定要删除这个配置文件?</string>
<string name="share_qr_nfc">二维码 / NFC</string>
<string name="copy_url">复制 URL</string>
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string>
<string name="add_profile_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 +143,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>
......@@ -95,19 +95,20 @@
<string name="delete">刪除</string>
<string name="delete_confirm_prompt">您確定要移除這個設定檔嗎?</string>
<string name="share_qr_nfc">QR 碼 / NFC</string>
<string name="copy_url">複製 URL</string>
<string name="add_profile_dialog">為 Shadowsocks 新增此設定檔?</string>
<string name="add_profile_methods_scan_qr_code">掃描 QR 碼</string>
<string name="add_profile_methods_manual_settings">手動設定</string>
<string 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 +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>
......@@ -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 &amp; 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>
......@@ -128,7 +130,6 @@
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="add_profile_scanner_not_installed">Please install any ZXing-compliant QR code scanning app.</string>
<string name="copy_url">Copy URL</string>
<plurals name="removed">
<item quantity="one">Removed</item>
<item quantity="other">%d items removed</item>
......@@ -156,4 +157,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,7 +133,8 @@ trait BaseService extends Service {
false
} else true
def connect(): Unit = if (profile.host == "198.199.101.152") {
def connect() {
if (profile.host == "198.199.101.152") {
val holder = app.containerHolder
val container = holder.getContainer
val url = container.getString("proxy_url")
......@@ -155,16 +157,16 @@ trait BaseService extends Service {
.post(requestBody)
.build()
val resposne = client.newCall(request).execute()
val list = resposne.body.string
val proxies = Random.shuffle(list.split('|').toSeq)
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) {
this.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,20 @@ 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()
val bundle = new Bundle()
bundle.putString(KEY_URL, url)
setArguments(bundle)
}
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 && profile.route != Acl.CUSTOM_RULES)
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 && profile.route != Acl.CUSTOM_RULES)
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)
......
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)
}
......@@ -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 =>
......
......@@ -18,7 +18,7 @@
/* */
/*******************************************************************************/
package com.github.shadowsocks.job
package com.github.shadowsocks.acl
import android.util.Log
import com.evernote.android.job.JobCreator
......
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))
}
......@@ -22,7 +22,6 @@ package com.github.shadowsocks.widget
import android.support.design.widget.Snackbar
import android.view.View
import com.github.shadowsocks.R
import scala.collection.mutable.ArrayBuffer
......@@ -51,12 +50,13 @@ 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)
.setCallback(removedCallback).setAction(R.string.undo, (_ => {
.addCallback(removedCallback)
.setAction(R.string.undo, (_ => {
undo(recycleBin.reverseIterator)
recycleBin.clear
}): View.OnClickListener)
......
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))
}
}
}
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